diff options
Diffstat (limited to 'test/ruby')
110 files changed, 6676 insertions, 3051 deletions
diff --git a/test/ruby/namespace/a.1_1_0.rb b/test/ruby/box/a.1_1_0.rb index bf64dbaa62..0322585097 100644 --- a/test/ruby/namespace/a.1_1_0.rb +++ b/test/ruby/box/a.1_1_0.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class NS_A +class BOX_A VERSION = "1.1.0" def yay @@ -8,7 +8,7 @@ class NS_A end end -module NS_B +module BOX_B VERSION = "1.1.0" def self.yay diff --git a/test/ruby/namespace/a.1_2_0.rb b/test/ruby/box/a.1_2_0.rb index 6d25c0885d..29813ea57b 100644 --- a/test/ruby/namespace/a.1_2_0.rb +++ b/test/ruby/box/a.1_2_0.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class NS_A +class BOX_A VERSION = "1.2.0" def yay @@ -8,7 +8,7 @@ class NS_A end end -module NS_B +module BOX_B VERSION = "1.2.0" def self.yay diff --git a/test/ruby/namespace/a.rb b/test/ruby/box/a.rb index a6dcd9cd21..26a622c92b 100644 --- a/test/ruby/namespace/a.rb +++ b/test/ruby/box/a.rb @@ -1,4 +1,4 @@ -class NS_A +class BOX_A FOO = "foo_a1" def yay @@ -6,7 +6,7 @@ class NS_A end end -module NS_B +module BOX_B BAR = "bar_b1" def self.yay diff --git a/test/ruby/namespace/autoloading.rb b/test/ruby/box/autoloading.rb index 19ec00bcd5..cba57ab377 100644 --- a/test/ruby/namespace/autoloading.rb +++ b/test/ruby/box/autoloading.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -autoload :NS_A, File.join(__dir__, 'a.1_1_0') -NS_A.new.yay +autoload :BOX_A, File.join(__dir__, 'a.1_1_0') +BOX_A.new.yay -module NS_B +module BOX_B autoload :BAR, File.join(__dir__, 'a') end diff --git a/test/ruby/namespace/blank.rb b/test/ruby/box/blank.rb index 6d201b0966..6d201b0966 100644 --- a/test/ruby/namespace/blank.rb +++ b/test/ruby/box/blank.rb diff --git a/test/ruby/namespace/blank1.rb b/test/ruby/box/blank1.rb index 6d201b0966..6d201b0966 100644 --- a/test/ruby/namespace/blank1.rb +++ b/test/ruby/box/blank1.rb diff --git a/test/ruby/namespace/blank2.rb b/test/ruby/box/blank2.rb index ba38c1d6db..ba38c1d6db 100644 --- a/test/ruby/namespace/blank2.rb +++ b/test/ruby/box/blank2.rb diff --git a/test/ruby/box/box.rb b/test/ruby/box/box.rb new file mode 100644 index 0000000000..3b7da14e9d --- /dev/null +++ b/test/ruby/box/box.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +BOX1 = Ruby::Box.new +BOX1.require_relative('a.1_1_0') + +def yay + BOX1::BOX_B::yay +end + +yay diff --git a/test/ruby/namespace/call_proc.rb b/test/ruby/box/call_proc.rb index 8acf538fc1..8acf538fc1 100644 --- a/test/ruby/namespace/call_proc.rb +++ b/test/ruby/box/call_proc.rb diff --git a/test/ruby/namespace/call_toplevel.rb b/test/ruby/box/call_toplevel.rb index c311a37028..c311a37028 100644 --- a/test/ruby/namespace/call_toplevel.rb +++ b/test/ruby/box/call_toplevel.rb diff --git a/test/ruby/namespace/consts.rb b/test/ruby/box/consts.rb index 44a383111b..e40cd5c50c 100644 --- a/test/ruby/namespace/consts.rb +++ b/test/ruby/box/consts.rb @@ -1,3 +1,4 @@ +$VERBOSE = nil class String STR_CONST1 = 111 STR_CONST2 = 222 diff --git a/test/ruby/namespace/define_toplevel.rb b/test/ruby/box/define_toplevel.rb index aa77db3a13..aa77db3a13 100644 --- a/test/ruby/namespace/define_toplevel.rb +++ b/test/ruby/box/define_toplevel.rb diff --git a/test/ruby/namespace/global_vars.rb b/test/ruby/box/global_vars.rb index 3764eb0d19..590363f617 100644 --- a/test/ruby/namespace/global_vars.rb +++ b/test/ruby/box/global_vars.rb @@ -20,18 +20,18 @@ end module UniqueGvar def self.read - $used_only_in_ns + $used_only_in_box end def self.write(val) - $used_only_in_ns = val + $used_only_in_box = val end def self.write_only(val) - $write_only_var_in_ns = val + $write_only_var_in_box = val end - def self.gvars_in_ns + def self.gvars_in_box global_variables end end diff --git a/test/ruby/namespace/instance_variables.rb b/test/ruby/box/instance_variables.rb index 1562ad5d45..1562ad5d45 100644 --- a/test/ruby/namespace/instance_variables.rb +++ b/test/ruby/box/instance_variables.rb diff --git a/test/ruby/namespace/line_splitter.rb b/test/ruby/box/line_splitter.rb index 2596975ad7..2596975ad7 100644 --- a/test/ruby/namespace/line_splitter.rb +++ b/test/ruby/box/line_splitter.rb diff --git a/test/ruby/namespace/load_path.rb b/test/ruby/box/load_path.rb index 7e5a83ef96..7e5a83ef96 100644 --- a/test/ruby/namespace/load_path.rb +++ b/test/ruby/box/load_path.rb diff --git a/test/ruby/namespace/open_class_with_include.rb b/test/ruby/box/open_class_with_include.rb index ad8fd58ea0..ad8fd58ea0 100644 --- a/test/ruby/namespace/open_class_with_include.rb +++ b/test/ruby/box/open_class_with_include.rb diff --git a/test/ruby/namespace/proc_callee.rb b/test/ruby/box/proc_callee.rb index d30ab5d9f3..d30ab5d9f3 100644 --- a/test/ruby/namespace/proc_callee.rb +++ b/test/ruby/box/proc_callee.rb diff --git a/test/ruby/namespace/proc_caller.rb b/test/ruby/box/proc_caller.rb index 8acf538fc1..8acf538fc1 100644 --- a/test/ruby/namespace/proc_caller.rb +++ b/test/ruby/box/proc_caller.rb diff --git a/test/ruby/namespace/procs.rb b/test/ruby/box/procs.rb index a7fe58ceb6..1c39a8231b 100644 --- a/test/ruby/namespace/procs.rb +++ b/test/ruby/box/procs.rb @@ -11,7 +11,7 @@ module ProcLookupTestA end end -module ProcInNS +module ProcInBox def self.make_proc_from_block(&b) b end diff --git a/test/ruby/namespace/raise.rb b/test/ruby/box/raise.rb index efb67f85c5..efb67f85c5 100644 --- a/test/ruby/namespace/raise.rb +++ b/test/ruby/box/raise.rb diff --git a/test/ruby/namespace/returns_proc.rb b/test/ruby/box/returns_proc.rb index bb816e5024..bb816e5024 100644 --- a/test/ruby/namespace/returns_proc.rb +++ b/test/ruby/box/returns_proc.rb diff --git a/test/ruby/namespace/singleton_methods.rb b/test/ruby/box/singleton_methods.rb index 05470932d2..05470932d2 100644 --- a/test/ruby/namespace/singleton_methods.rb +++ b/test/ruby/box/singleton_methods.rb diff --git a/test/ruby/namespace/string_ext.rb b/test/ruby/box/string_ext.rb index d8c5a3d661..d8c5a3d661 100644 --- a/test/ruby/namespace/string_ext.rb +++ b/test/ruby/box/string_ext.rb diff --git a/test/ruby/namespace/string_ext_caller.rb b/test/ruby/box/string_ext_caller.rb index b8345d98ed..b8345d98ed 100644 --- a/test/ruby/namespace/string_ext_caller.rb +++ b/test/ruby/box/string_ext_caller.rb diff --git a/test/ruby/namespace/string_ext_calling.rb b/test/ruby/box/string_ext_calling.rb index 6467b728dd..6467b728dd 100644 --- a/test/ruby/namespace/string_ext_calling.rb +++ b/test/ruby/box/string_ext_calling.rb diff --git a/test/ruby/namespace/string_ext_eval_caller.rb b/test/ruby/box/string_ext_eval_caller.rb index 0e6b20c19f..0e6b20c19f 100644 --- a/test/ruby/namespace/string_ext_eval_caller.rb +++ b/test/ruby/box/string_ext_eval_caller.rb diff --git a/test/ruby/namespace/top_level.rb b/test/ruby/box/top_level.rb index 90df145578..90df145578 100644 --- a/test/ruby/namespace/top_level.rb +++ b/test/ruby/box/top_level.rb diff --git a/test/ruby/enc/test_emoji_breaks.rb b/test/ruby/enc/test_emoji_breaks.rb index bb5114680e..0873e681c3 100644 --- a/test/ruby/enc/test_emoji_breaks.rb +++ b/test/ruby/enc/test_emoji_breaks.rb @@ -53,7 +53,7 @@ class TestEmojiBreaks < Test::Unit::TestCase EMOJI_DATA_FILES = %w[emoji-sequences emoji-test emoji-zwj-sequences].map do |basename| BreakFile.new(basename, EMOJI_DATA_PATH, EMOJI_VERSION) end - UNICODE_DATA_FILE = BreakFile.new('emoji-variation-sequences', UNICODE_DATA_PATH, UNICODE_VERSION) + UNICODE_DATA_FILE = BreakFile.new('emoji-variation-sequences', UNICODE_DATA_PATH, EMOJI_VERSION) EMOJI_DATA_FILES << UNICODE_DATA_FILE def self.data_files_available? diff --git a/test/ruby/namespace/current.rb b/test/ruby/namespace/current.rb deleted file mode 100644 index 4af9a92ddc..0000000000 --- a/test/ruby/namespace/current.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -$ns_in_ns = ::Namespace.current - -module CurrentNamespace - def self.in_require - $ns_in_ns - end - - def self.in_method_call - ::Namespace.current - end -end diff --git a/test/ruby/namespace/ns.rb b/test/ruby/namespace/ns.rb deleted file mode 100644 index e947e3cdc8..0000000000 --- a/test/ruby/namespace/ns.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -NS1 = Namespace.new -NS1.require_relative('a.1_1_0') - -def yay - NS1::NS_B::yay -end - -yay diff --git a/test/ruby/sentence.rb b/test/ruby/sentence.rb index 9bfd7c7599..99ced05d2f 100644 --- a/test/ruby/sentence.rb +++ b/test/ruby/sentence.rb @@ -211,7 +211,7 @@ class Sentence # returns new sentence object which # _target_ is substituted by the block. # - # Sentence#subst invokes <tt>_target_ === _string_</tt> for each + # Sentence#subst invokes <tt>target === string</tt> for each # string in the sentence. # The strings which === returns true are substituted by the block. # The block is invoked with the substituting string. diff --git a/test/ruby/test_allocation.rb b/test/ruby/test_allocation.rb index a2ccd7bd65..90d7c04f9b 100644 --- a/test/ruby/test_allocation.rb +++ b/test/ruby/test_allocation.rb @@ -66,9 +66,7 @@ class TestAllocation < Test::Unit::TestCase #{checks} - unless failures.empty? - assert_equal(true, false, failures.join("\n")) - end + assert_empty(failures) RUBY end @@ -529,6 +527,59 @@ class TestAllocation < Test::Unit::TestCase RUBY end + def test_anonymous_splat_parameter + only_block = block.empty? ? block : block[2..] + check_allocations(<<~RUBY) + def self.anon_splat(*#{block}); end + + check_allocations(1, 1, "anon_splat(1, a: 2#{block})") + check_allocations(1, 1, "anon_splat(1, *empty_array, a: 2#{block})") + check_allocations(1, 1, "anon_splat(1, a:2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, **empty_hash, a: 2#{block})") + + check_allocations(1, 0, "anon_splat(1, **nil#{block})") + check_allocations(1, 0, "anon_splat(1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, **hash1#{block})") + check_allocations(1, 1, "anon_splat(1, *empty_array, **hash1#{block})") + check_allocations(1, 1, "anon_splat(1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, **empty_hash, **hash1#{block})") + + check_allocations(1, 0, "anon_splat(1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "anon_splat(*array1, a: 2#{block})") + + check_allocations(0, 0, "anon_splat(*nil, **nill#{block})") + check_allocations(0, 0, "anon_splat(*array1, **nill#{block})") + check_allocations(0, 0, "anon_splat(*array1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(*array1, **hash1#{block})") + check_allocations(1, 1, "anon_splat(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "anon_splat(*array1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "anon_splat(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 0, "anon_splat(#{only_block})") + check_allocations(1, 1, "anon_splat(a: 2#{block})") + check_allocations(0, 0, "anon_splat(**empty_hash#{block})") + + check_allocations(1, 1, "anon_splat(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(*array1, **empty_hash, a: 2#{block})") + check_allocations(1, 1, "anon_splat(*array1, **hash1, **empty_hash#{block})") + + unless defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled? + check_allocations(0, 0, "anon_splat(*array1, **nil#{block})") + check_allocations(1, 0, "anon_splat(*r2k_empty_array#{block})") + check_allocations(1, 1, "anon_splat(*r2k_array#{block})") + check_allocations(1, 0, "anon_splat(*r2k_empty_array1#{block})") + check_allocations(1, 1, "anon_splat(*r2k_array1#{block})") + end + RUBY + end + def test_anonymous_splat_and_anonymous_keyword_splat_parameters only_block = block.empty? ? block : block[2..] check_allocations(<<~RUBY) diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index a3ac0a6a0b..76455187a5 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -1336,6 +1336,28 @@ class TestArray < Test::Unit::TestCase assert_equal(@cls[@cls[1,2], nil, 'dog', 'cat'], a.prepend(@cls[1, 2])) end + def test_tolerant_to_redefinition + *code = __FILE__, __LINE__+1, "#{<<-"{#"}\n#{<<-'};'}" + {# + module M + def <<(a) + super(a * 2) + end + end + class Array; prepend M; end + ary = [*1..10] + mapped = ary.map {|i| i} + selected = ary.select {true} + module M + remove_method :<< + end + assert_equal(ary, mapped) + assert_equal(ary, selected) + }; + assert_separately(%w[--disable-yjit], *code) + assert_separately(%w[--enable-yjit], *code) + end + def test_push a = @cls[1, 2, 3] assert_equal(@cls[1, 2, 3, 4, 5], a.push(4, 5)) @@ -1824,19 +1846,21 @@ class TestArray < Test::Unit::TestCase assert_equal([1, 2, 3, 4], a) end - def test_freeze_inside_sort! + def test_freeze_inside_sort_bang array = [1, 2, 3, 4, 5] frozen_array = nil assert_raise(FrozenError) do count = 0 array.sort! do |a, b| - array.freeze if (count += 1) == 6 + array.freeze if (count += 1) == 3 frozen_array ||= array.map.to_a if array.frozen? b <=> a end end assert_equal(frozen_array, array) + end + def test_freeze_inside_sort_bang_non_numeric_block object = Object.new array = [1, 2, 3, 4, 5] object.define_singleton_method(:>){|_| array.freeze; true} @@ -1845,7 +1869,9 @@ class TestArray < Test::Unit::TestCase object end end + end + def test_freeze_inside_sort_bang_non_numeric_no_block object = Object.new array = [object, object] object.define_singleton_method(:>){|_| array.freeze; true} @@ -3524,6 +3550,7 @@ class TestArray < Test::Unit::TestCase assert_float_equal(3.5, [3].sum(0.5)) assert_float_equal(8.5, [3.5, 5].sum) assert_float_equal(10.5, [2, 8.5].sum) + assert_float_equal(1_000 * 0.1, Array.new(1_000, 0.1).sum(0.0)) assert_float_equal((FIXNUM_MAX+1).to_f, [FIXNUM_MAX, 1, 0.0].sum) assert_float_equal((FIXNUM_MAX+1).to_f, [0.0, FIXNUM_MAX+1].sum) @@ -3584,6 +3611,23 @@ class TestArray < Test::Unit::TestCase assert_equal((1..67).to_a.reverse, var_0) end + def test_find + ary = [1, 2, 3, 4, 5] + assert_equal(2, ary.find {|x| x % 2 == 0 }) + assert_equal(nil, ary.find {|x| false }) + assert_equal(:foo, ary.find(proc { :foo }) {|x| false }) + end + + def test_rfind + ary = [1, 2, 3, 4, 5] + assert_equal(4, ary.rfind {|x| x % 2 == 0 }) + assert_equal(1, ary.rfind {|x| x < 2 }) + assert_equal(5, ary.rfind {|x| x > 4 }) + assert_equal(nil, ary.rfind {|x| false }) + assert_equal(:foo, ary.rfind(proc { :foo }) {|x| false }) + assert_equal(nil, ary.rfind {|x| ary.clear; false }) + end + private def need_continuation unless respond_to?(:callcc, true) diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index 9a7d75c270..8b9a3f615d 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -48,7 +48,7 @@ class TestAst < Test::Unit::TestCase @path = path @errors = [] @debug = false - @ast = RubyVM::AbstractSyntaxTree.parse(src) if src + @ast = EnvUtil.suppress_warning { RubyVM::AbstractSyntaxTree.parse(src) } if src end def validate_range @@ -67,7 +67,7 @@ class TestAst < Test::Unit::TestCase def ast return @ast if defined?(@ast) - @ast = RubyVM::AbstractSyntaxTree.parse_file(@path) + @ast = EnvUtil.suppress_warning { RubyVM::AbstractSyntaxTree.parse_file(@path) } end private @@ -135,7 +135,7 @@ class TestAst < Test::Unit::TestCase Dir.glob("test/**/*.rb", base: SRCDIR).each do |path| define_method("test_all_tokens:#{path}") do - node = RubyVM::AbstractSyntaxTree.parse_file("#{SRCDIR}/#{path}", keep_tokens: true) + node = EnvUtil.suppress_warning { RubyVM::AbstractSyntaxTree.parse_file("#{SRCDIR}/#{path}", keep_tokens: true) } tokens = node.all_tokens.sort_by { [_1.last[0], _1.last[1]] } tokens_bytes = tokens.map { _1[2]}.join.bytes source_bytes = File.read("#{SRCDIR}/#{path}").bytes @@ -215,6 +215,17 @@ class TestAst < Test::Unit::TestCase end end + def test_cdecl_children_with_toplevel_constant_path + # [Bug #21974] + children = parse("::Foo = 1").children[2].children + + assert_equal(:COLON3, children[0].type) + assert_equal([:Foo], children[0].children) + assert_equal(:Foo, children[1]) + assert_equal(:INTEGER, children[2].type) + assert_equal([1], children[2].children) + end + def assert_parse(code, warning: '') node = assert_warning(warning) {RubyVM::AbstractSyntaxTree.parse(code)} assert_kind_of(RubyVM::AbstractSyntaxTree::Node, node, code) @@ -244,7 +255,8 @@ class TestAst < Test::Unit::TestCase assert_invalid_parse(msg, "#{code}") assert_invalid_parse(msg, "def m; #{code}; end") assert_invalid_parse(msg, "begin; #{code}; end") - assert_parse("END {#{code}}") + assert_invalid_parse(msg, "BEGIN {#{code}}") + assert_invalid_parse(msg, "END {#{code}}") assert_parse("!defined?(#{code})") assert_parse("def m; defined?(#{code}); end") @@ -365,6 +377,50 @@ class TestAst < Test::Unit::TestCase assert_equal node.node_id, node_id end + def add(x, y) + end + + def test_node_id_for_backtrace_location_of_method_definition + omit if ParserSupport.prism_enabled? + + begin + add(1) + rescue ArgumentError => exc + loc = exc.backtrace_locations.first + node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(loc) + node = RubyVM::AbstractSyntaxTree.of(method(:add)) + assert_equal node.node_id, node_id + end + end + + def test_node_id_for_backtrace_location_of_lambda + omit if ParserSupport.prism_enabled? + + v = -> {} + begin + v.call(1) + rescue ArgumentError => exc + loc = exc.backtrace_locations.first + node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(loc) + node = RubyVM::AbstractSyntaxTree.of(v) + assert_equal node.node_id, node_id + end + end + + def test_node_id_for_backtrace_location_of_lambda_method + omit if ParserSupport.prism_enabled? + + v = lambda {} + begin + v.call(1) + rescue ArgumentError => exc + loc = exc.backtrace_locations.first + node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(loc) + node = RubyVM::AbstractSyntaxTree.of(v) + assert_equal node.node_id, node_id + end + end + def test_node_id_for_backtrace_location_raises_argument_error bug19262 = '[ruby-core:111435]' @@ -665,6 +721,7 @@ class TestAst < Test::Unit::TestCase assert_equal(nil, block_arg.call('')) assert_equal(:block, block_arg.call('&block')) assert_equal(:&, block_arg.call('&')) + assert_equal(false, block_arg.call('&nil')) end def test_keyword_rest @@ -801,7 +858,7 @@ dummy node_proc = RubyVM::AbstractSyntaxTree.of(proc, keep_script_lines: true) node_method = RubyVM::AbstractSyntaxTree.of(method, keep_script_lines: true) - assert_equal("{ 1 + 2 }", node_proc.source) + assert_equal("Proc.new { 1 + 2 }", node_proc.source) assert_equal("def test_keep_script_lines_for_of\n", node_method.source.lines.first) end @@ -878,7 +935,7 @@ dummy omit if ParserSupport.prism_enabled? || ParserSupport.prism_enabled_in_subprocess? assert_in_out_err(["-e", "def foo; end; pp RubyVM::AbstractSyntaxTree.of(method(:foo)).type"], - "", [":SCOPE"], []) + "", [":DEFN"], []) end def test_error_tolerant @@ -1186,7 +1243,7 @@ dummy args: nil body: (LAMBDA@1:0-2:3 - (SCOPE@1:2-2:3 + (SCOPE@1:0-2:3 tbl: [] args: (ARGS@1:2-1:2 @@ -1592,6 +1649,14 @@ dummy assert_locations(node.children[-1].locations, [[1, 0, 1, 6], [1, 0, 1, 6]]) end + def test_sclass_locations + node = ast_parse("class << self; end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 18], [1, 0, 1, 5], [1, 6, 1, 8], [1, 15, 1, 18]]) + + node = ast_parse("class << obj; foo; end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 22], [1, 0, 1, 5], [1, 6, 1, 8], [1, 19, 1, 22]]) + end + def test_splat_locations node = ast_parse("a = *1") assert_locations(node.children[-1].children[1].locations, [[1, 4, 1, 6], [1, 4, 1, 5]]) @@ -1682,6 +1747,19 @@ dummy assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 9, 1, 20], [1, 9, 1, 14], [1, 14, 1, 15], [1, 19, 1, 20]]) end + def test_negative_numeric_locations + node = ast_parse("-1") + assert_locations(node.children.last.locations, [[1, 0, 1, 2]]) + end + + def test_numeric_location_with_nonsuffix + node = ast_parse("1if true") + assert_locations(node.children.last.children[1].locations, [[1, 0, 1, 1]]) + + node = ast_parse("1q", error_tolerant: true) + assert_locations(node.children.last.locations, [[1, 0, 1, 1]]) + end + private def ast_parse(src, **options) begin @@ -1695,7 +1773,7 @@ dummy def assert_locations(locations, expected) ary = locations.map {|loc| loc && [loc.first_lineno, loc.first_column, loc.last_lineno, loc.last_column] } - assert_equal(ary, expected) + assert_equal(expected, ary) end end end diff --git a/test/ruby/test_autoload.rb b/test/ruby/test_autoload.rb index 607f0e3355..de08be96e4 100644 --- a/test/ruby/test_autoload.rb +++ b/test/ruby/test_autoload.rb @@ -224,7 +224,7 @@ p Foo::Bar Kernel.module_eval do alias old_require require end - Namespace.module_eval do + Ruby::Box.module_eval do alias old_require require end called_with = [] @@ -232,7 +232,7 @@ p Foo::Bar called_with << path old_require path end - Namespace.send :define_method, :require do |path| + Ruby::Box.send :define_method, :require do |path| called_with << path old_require path end @@ -243,7 +243,7 @@ p Foo::Bar alias require old_require undef old_require end - Namespace.module_eval do + Ruby::Box.module_eval do undef require alias require old_require undef old_require @@ -574,7 +574,7 @@ p Foo::Bar autoload_path = File.join(tmpdir, "autoload_parallel_race.rb") File.write(autoload_path, 'module Foo; end; module Bar; end') - assert_separately([], <<-RUBY, timeout: 100) + assert_ruby_status([], <<-RUBY, timeout: 100) autoload_path = #{File.realpath(autoload_path).inspect} # This should work with no errors or failures. @@ -613,4 +613,103 @@ p Foo::Bar RUBY end end + + def test_autoload_relative_toplevel + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'main.rb') + module_file = File.join(tmpdir, 'test_module.rb') + + File.write(module_file, <<-RUBY) + module AutoloadRelativeTest + VERSION = '1.0' + end + RUBY + + File.write(main_file, <<-RUBY) + autoload_relative :AutoloadRelativeTest, 'test_module.rb' + puts AutoloadRelativeTest::VERSION + RUBY + + assert_in_out_err([main_file], '', ['1.0'], []) + end + end + + def test_autoload_relative_module_level + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'main_mod.rb') + module_file = File.join(tmpdir, 'nested_module.rb') + + File.write(module_file, <<-RUBY) + module Container + module NestedModule + MSG = 'loaded' + end + end + RUBY + + File.write(main_file, <<-RUBY) + module Container + autoload_relative :NestedModule, 'nested_module.rb' + end + puts Container::NestedModule::MSG + RUBY + + assert_in_out_err([main_file], '', ['loaded'], []) + end + end + + def test_autoload_relative_query + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'query_test.rb') + module_file = File.join(tmpdir, 'query_module.rb') + + File.write(module_file, 'module QueryModule; end') + + File.write(main_file, <<-RUBY) + autoload_relative :QueryModule, 'query_module.rb' + path = autoload?(:QueryModule) + # Use realpath for comparison to handle symlinks (e.g., /var -> /private/var on macOS) + real_tmpdir = File.realpath('#{tmpdir}') + puts path.start_with?(real_tmpdir) && path.end_with?('query_module.rb') + RUBY + + assert_in_out_err([main_file], '', ['true'], []) + end + end + + def test_autoload_relative_nested_directory + Dir.mktmpdir('autoload_relative') do |tmpdir| + nested_dir = File.join(tmpdir, 'nested') + Dir.mkdir(nested_dir) + + main_file = File.join(tmpdir, 'nested_test.rb') + module_file = File.join(nested_dir, 'deep_module.rb') + + File.write(module_file, 'module DeepModule; VALUE = 42; end') + + File.write(main_file, <<-RUBY) + autoload_relative :DeepModule, 'nested/deep_module.rb' + puts DeepModule::VALUE + RUBY + + assert_in_out_err([main_file], '', ['42'], []) + end + end + + def test_autoload_relative_no_basepath + # Test that autoload_relative raises an error when called from eval without file context + assert_raise(LoadError) do + eval('autoload_relative :TestConst, "test.rb"') + end + end + + private + + def assert_separately(*args, **kwargs) + super(*args, timeout: 60, **kwargs) + end + + def assert_ruby_status(*args, **kwargs) + super(*args, timeout: 60, **kwargs) + end end diff --git a/test/ruby/test_backtrace.rb b/test/ruby/test_backtrace.rb index dad7dfcb55..332d76c58e 100644 --- a/test/ruby/test_backtrace.rb +++ b/test/ruby/test_backtrace.rb @@ -191,6 +191,16 @@ class TestBacktrace < Test::Unit::TestCase assert_equal(cl.map(&:to_s), ary.map(&:to_s)) end + def test_each_caller_location_single_cfunc_frame + assert_normal_exit <<~'RUBY' + tap { Thread.each_caller_location(1, 1) { |loc| loc.label } } + RUBY + + cl = nil; ary = [] + tap { cl = caller_locations(1, 1); Thread.each_caller_location(1, 1) { |x| ary << x } } + assert_equal(cl.map(&:to_s), ary.map(&:to_s)) + end + def test_caller_locations_first_label def self.label caller_locations.first.label diff --git a/test/ruby/test_beginendblock.rb b/test/ruby/test_beginendblock.rb index 3706efab52..74da11abf4 100644 --- a/test/ruby/test_beginendblock.rb +++ b/test/ruby/test_beginendblock.rb @@ -40,7 +40,8 @@ class TestBeginEndBlock < Test::Unit::TestCase assert_in_out_err([], "#{<<~"begin;"}#{<<~'end;'}", [], ['-:2: warning: END in method; use at_exit']) begin; def end1 - END {} + END { + } end end; end diff --git a/test/ruby/test_bignum.rb b/test/ruby/test_bignum.rb index dd6f4baa4c..c366f794b2 100644 --- a/test/ruby/test_bignum.rb +++ b/test/ruby/test_bignum.rb @@ -821,6 +821,9 @@ class TestBignum < Test::Unit::TestCase assert_equal([7215, 2413, 6242], T1024P.digits(10_000).first(3)) assert_equal([11], 11.digits(T1024P)) assert_equal([T1024P - 1, 1], (T1024P + T1024P - 1).digits(T1024P)) + bug21680 = '[ruby-core:123769] [Bug #21680]' + assert_equal([0] * 64 + [1], (2**512).digits(256), bug21680) + assert_equal([0] * 128 + [1], (123**128).digits(123), bug21680) end def test_digits_for_negative_numbers diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb new file mode 100644 index 0000000000..a425c5eb7d --- /dev/null +++ b/test/ruby/test_box.rb @@ -0,0 +1,1219 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'rbconfig' +require 'tempfile' + +class TestBox < Test::Unit::TestCase + EXPERIMENTAL_WARNING_LINE_PATTERNS = [ + /#{RbConfig::CONFIG["ruby_install_name"] || "ruby"}(\.exe)?: warning: Ruby::Box is experimental, and the behavior may change in the future!/, + %r{See https://docs.ruby-lang.org/en/(master|\d\.\d)/Ruby/Box.html for known issues, etc.} + ] + ENV_ENABLE_BOX = {'RUBY_BOX' => '1', 'TEST_DIR' => __dir__} + + def setup + @box = nil + @dir = __dir__ + end + + def teardown + @box = nil + end + + def setup_box + pend unless Ruby::Box.enabled? + @box = Ruby::Box.new + end + + def test_box_availability_in_default + assert_separately(['RUBY_BOX'=>nil], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + assert_nil ENV['RUBY_BOX'] + assert_not_predicate Ruby::Box, :enabled? + end; + end + + def test_box_availability_when_enabled + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + assert_equal '1', ENV['RUBY_BOX'] + assert_predicate Ruby::Box, :enabled? + end; + end + + def test_current_box_in_main + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + assert_equal Ruby::Box.main, Ruby::Box.current + assert_predicate Ruby::Box.main, :main? + end; + end + + def test_require_rb_separately + setup_box + + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + + @box.require(File.join(__dir__, 'box', 'a.1_1_0')) + + assert_not_nil @box::BOX_A + assert_not_nil @box::BOX_B + assert_equal "1.1.0", @box::BOX_A::VERSION + assert_equal "yay 1.1.0", @box::BOX_A.new.yay + assert_equal "1.1.0", @box::BOX_B::VERSION + assert_equal "yay_b1", @box::BOX_B.yay + + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + end + + def test_require_relative_rb_separately + setup_box + + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + + @box.require_relative('box/a.1_1_0') + + assert_not_nil @box::BOX_A + assert_not_nil @box::BOX_B + assert_equal "1.1.0", @box::BOX_A::VERSION + assert_equal "yay 1.1.0", @box::BOX_A.new.yay + assert_equal "1.1.0", @box::BOX_B::VERSION + assert_equal "yay_b1", @box::BOX_B.yay + + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + end + + def test_load_separately + setup_box + + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + + @box.load(File.join(__dir__, 'box', 'a.1_1_0.rb')) + + assert_not_nil @box::BOX_A + assert_not_nil @box::BOX_B + assert_equal "1.1.0", @box::BOX_A::VERSION + assert_equal "yay 1.1.0", @box::BOX_A.new.yay + assert_equal "1.1.0", @box::BOX_B::VERSION + assert_equal "yay_b1", @box::BOX_B.yay + + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + end + + def test_box_in_box + setup_box + + assert_raise(NameError) { BOX1 } + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + + @box.require_relative('box/box') + + assert_not_nil @box::BOX1 + assert_not_nil @box::BOX1::BOX_A + assert_not_nil @box::BOX1::BOX_B + assert_equal "1.1.0", @box::BOX1::BOX_A::VERSION + assert_equal "yay 1.1.0", @box::BOX1::BOX_A.new.yay + assert_equal "1.1.0", @box::BOX1::BOX_B::VERSION + assert_equal "yay_b1", @box::BOX1::BOX_B.yay + + assert_raise(NameError) { BOX1 } + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + end + + def test_require_rb_2versiobox + setup_box + + assert_raise(NameError) { BOX_A } + + @box.require(File.join(__dir__, 'box', 'a.1_2_0')) + assert_equal "1.2.0", @box::BOX_A::VERSION + assert_equal "yay 1.2.0", @box::BOX_A.new.yay + + n2 = Ruby::Box.new + n2.require(File.join(__dir__, 'box', 'a.1_1_0')) + assert_equal "1.1.0", n2::BOX_A::VERSION + assert_equal "yay 1.1.0", n2::BOX_A.new.yay + + # recheck @box is not affected by the following require + assert_equal "1.2.0", @box::BOX_A::VERSION + assert_equal "yay 1.2.0", @box::BOX_A.new.yay + + assert_raise(NameError) { BOX_A } + end + + def test_raising_errors_in_require + setup_box + + assert_raise(RuntimeError, "Yay!") { @box.require(File.join(__dir__, 'box', 'raise')) } + assert_include Ruby::Box.current.inspect, "main" + end + + def test_class_variables_in_root_are_invisible_in_other_boxes + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + Ruby::Box.root.eval(<<~RUBY) + module M + @@x = 1 + end + + class A + include M + end + + class B < A + end + RUBY + + code = <<~REPRO + class ::B + @@x += 1 + end + REPRO + + b1 = Ruby::Box.new + assert_raise(NameError, "uninitialized class variable @@x in B") { + b1.eval(code) + } + end; + end + + def test_autoload_in_box + setup_box + + assert_raise(NameError) { BOX_A } + + @box.require_relative('box/autoloading') + # autoloaded A is visible from global + assert_equal '1.1.0', @box::BOX_A::VERSION + + assert_raise(NameError) { BOX_A } + + # autoload trigger BOX_B::BAR is valid even from global + assert_equal 'bar_b1', @box::BOX_B::BAR + + assert_raise(NameError) { BOX_A } + assert_raise(NameError) { BOX_B } + end + + def test_continuous_top_level_method_in_a_box + setup_box + + @box.require_relative('box/define_toplevel') + @box.require_relative('box/call_toplevel') + + assert_raise(NameError) { foo } + end + + def test_top_level_methods_in_box + pend # TODO: fix loading/current box detection + setup_box + @box.require_relative('box/top_level') + assert_equal "yay!", @box::Foo.foo + assert_raise(NameError) { yaaay } + assert_equal "foo", @box::Bar.bar + assert_raise_with_message(RuntimeError, "boooo") { @box::Baz.baz } + end + + def test_proc_defined_in_box_refers_module_in_box + setup_box + + # require_relative dosn't work well in assert_separately even with __FILE__ and __LINE__ + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + box1 = Ruby::Box.new + box1.require("#{here}/box/proc_callee") + proc_v = box1::Foo.callee + assert_raise(NameError) { Target } + assert box1::Target + assert_equal "fooooo", proc_v.call # refers Target in the box box1 + box1.require("#{here}/box/proc_caller") + assert_equal "fooooo", box1::Bar.caller(proc_v) + + box2 = Ruby::Box.new + box2.require("#{here}/box/proc_caller") + assert_raise(NameError) { box2::Target } + assert_equal "fooooo", box2::Bar.caller(proc_v) # refers Target in the box box1 + end; + end + + def test_proc_defined_globally_refers_global_module + setup_box + + # require_relative dosn't work well in assert_separately even with __FILE__ and __LINE__ + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + require("#{here}/box/proc_callee") + def Target.foo + "yay" + end + proc_v = Foo.callee + assert Target + assert_equal "yay", proc_v.call # refers global Foo + box1 = Ruby::Box.new + box1.require("#{here}/box/proc_caller") + assert_equal "yay", box1::Bar.caller(proc_v) + + box2 = Ruby::Box.new + box2.require("#{here}/box/proc_callee") + box2.require("#{here}/box/proc_caller") + assert_equal "fooooo", box2::Foo.callee.call + assert_equal "yay", box2::Bar.caller(proc_v) # should refer the global Target, not Foo in box2 + end; + end + + def test_instance_variable + setup_box + + @box.require_relative('box/instance_variables') + + assert_equal [], String.instance_variables + assert_equal [:@str_ivar1, :@str_ivar2], @box::StringDelegatorObj.instance_variables + assert_equal 111, @box::StringDelegatorObj.str_ivar1 + assert_equal 222, @box::StringDelegatorObj.str_ivar2 + assert_equal 222, @box::StringDelegatorObj.instance_variable_get(:@str_ivar2) + + @box::StringDelegatorObj.instance_variable_set(:@str_ivar3, 333) + assert_equal 333, @box::StringDelegatorObj.instance_variable_get(:@str_ivar3) + @box::StringDelegatorObj.remove_instance_variable(:@str_ivar1) + assert_nil @box::StringDelegatorObj.str_ivar1 + assert_equal [:@str_ivar2, :@str_ivar3], @box::StringDelegatorObj.instance_variables + + assert_equal [], String.instance_variables + end + + def test_methods_added_in_box_are_invisible_globally + setup_box + + @box.require_relative('box/string_ext') + + assert_equal "yay", @box::Bar.yay + + assert_raise(NoMethodError){ String.new.yay } + end + + def test_continuous_method_definitions_in_a_box + setup_box + + @box.require_relative('box/string_ext') + assert_equal "yay", @box::Bar.yay + + @box.require_relative('box/string_ext_caller') + assert_equal "yay", @box::Foo.yay + + @box.require_relative('box/string_ext_calling') + end + + def test_methods_added_in_box_later_than_caller_code + setup_box + + @box.require_relative('box/string_ext_caller') + @box.require_relative('box/string_ext') + + assert_equal "yay", @box::Bar.yay + assert_equal "yay", @box::Foo.yay + end + + def test_method_added_in_box_are_available_on_eval + setup_box + + @box.require_relative('box/string_ext') + @box.require_relative('box/string_ext_eval_caller') + + assert_equal "yay", @box::Baz.yay + end + + def test_method_added_in_box_are_available_on_eval_with_binding + setup_box + + @box.require_relative('box/string_ext') + @box.require_relative('box/string_ext_eval_caller') + + assert_equal "yay, yay!", @box::Baz.yay_with_binding + end + + def test_methods_and_constants_added_by_include + setup_box + + @box.require_relative('box/open_class_with_include') + + assert_equal "I'm saying foo 1", @box::OpenClassWithInclude.say + assert_equal "I'm saying foo 1", @box::OpenClassWithInclude.say_foo + assert_equal "I'm saying foo 1", @box::OpenClassWithInclude.say_with_obj("wow") + + assert_raise(NameError) { String::FOO } + + assert_equal "foo 1", @box::OpenClassWithInclude.refer_foo + end +end + +module ProcLookupTestA + module B + VALUE = 111 + end +end + +class TestBox < Test::Unit::TestCase + def make_proc_from_block(&b) + b + end + + def test_proc_from_main_works_with_global_definitions + setup_box + + @box.require_relative('box/procs') + + proc_and_labels = [ + [Proc.new { String.new.yay }, "Proc.new"], + [proc { String.new.yay }, "proc{}"], + [lambda { String.new.yay }, "lambda{}"], + [->(){ String.new.yay }, "->(){}"], + [make_proc_from_block { String.new.yay }, "make_proc_from_block"], + [@box::ProcInBox.make_proc_from_block { String.new.yay }, "make_proc_from_block in @box"], + ] + + proc_and_labels.each do |str_pr| + pr, pr_label = str_pr + assert_raise(NoMethodError, "NoMethodError expected: #{pr_label}, called in main") { pr.call } + assert_raise(NoMethodError, "NoMethodError expected: #{pr_label}, called in @box") { @box::ProcInBox.call_proc(pr) } + end + + const_and_labels = [ + [Proc.new { ProcLookupTestA::B::VALUE }, "Proc.new"], + [proc { ProcLookupTestA::B::VALUE }, "proc{}"], + [lambda { ProcLookupTestA::B::VALUE }, "lambda{}"], + [->(){ ProcLookupTestA::B::VALUE }, "->(){}"], + [make_proc_from_block { ProcLookupTestA::B::VALUE }, "make_proc_from_block"], + [@box::ProcInBox.make_proc_from_block { ProcLookupTestA::B::VALUE }, "make_proc_from_block in @box"], + ] + + const_and_labels.each do |const_pr| + pr, pr_label = const_pr + assert_equal 111, pr.call, "111 expected, #{pr_label} called in main" + assert_equal 111, @box::ProcInBox.call_proc(pr), "111 expected, #{pr_label} called in @box" + end + end + + def test_proc_from_box_works_with_definitions_in_box + setup_box + + @box.require_relative('box/procs') + + proc_types = [:proc_new, :proc_f, :lambda_f, :lambda_l, :block] + + proc_types.each do |proc_type| + assert_equal 222, @box::ProcInBox.make_const_proc(proc_type).call, "ProcLookupTestA::B::VALUE should be 222 in @box" + assert_equal "foo", @box::ProcInBox.make_str_const_proc(proc_type).call, "String::FOO should be \"foo\" in @box" + assert_equal "yay", @box::ProcInBox.make_str_proc(proc_type).call, "String#yay should be callable in @box" + # + # TODO: method calls not-in-methods nor procs can't handle the current box correctly. + # + # assert_equal "yay,foo,222", + # @box::ProcInBox.const_get(('CONST_' + proc_type.to_s.upcase).to_sym).call, + # "Proc assigned to constants should refer constants correctly in @box" + end + end + + def test_class_module_singleton_methods + setup_box + + @box.require_relative('box/singleton_methods') + + assert_equal "Good evening!", @box::SingletonMethods.string_greeing # def self.greeting + assert_equal 42, @box::SingletonMethods.integer_answer # class << self; def answer + assert_equal([], @box::SingletonMethods.array_blank) # def self.blank w/ instance methods + assert_equal({status: 200, body: 'OK'}, @box::SingletonMethods.hash_http_200) # class << self; def ... w/ instance methods + + assert_equal([4, 4], @box::SingletonMethods.array_instance_methods_return_size([1, 2, 3, 4])) + assert_equal([3, 3], @box::SingletonMethods.hash_instance_methods_return_size({a: 2, b: 4, c: 8})) + + assert_raise(NoMethodError) { String.greeting } + assert_raise(NoMethodError) { Integer.answer } + assert_raise(NoMethodError) { Array.blank } + assert_raise(NoMethodError) { Hash.http_200 } + end + + def test_add_constants_in_box + setup_box + + @box.require('envutil') + + String.const_set(:STR_CONST0, 999) + assert_equal 999, String::STR_CONST0 + assert_equal 999, String.const_get(:STR_CONST0) + + assert_raise(NameError) { String.const_get(:STR_CONST1) } + assert_raise(NameError) { String::STR_CONST2 } + assert_raise(NameError) { String::STR_CONST3 } + assert_raise(NameError) { Integer.const_get(:INT_CONST1) } + + EnvUtil.verbose_warning do + @box.require_relative('box/consts') + end + + assert_equal 999, String::STR_CONST0 + assert_raise(NameError) { String::STR_CONST1 } + assert_raise(NameError) { String::STR_CONST2 } + assert_raise(NameError) { Integer::INT_CONST1 } + + assert_not_nil @box::ForConsts.refer_all + + assert_equal 112, @box::ForConsts.refer1 + assert_equal 112, @box::ForConsts.get1 + assert_equal 112, @box::ForConsts::CONST1 + assert_equal 222, @box::ForConsts.refer2 + assert_equal 222, @box::ForConsts.get2 + assert_equal 222, @box::ForConsts::CONST2 + assert_equal 333, @box::ForConsts.refer3 + assert_equal 333, @box::ForConsts.get3 + assert_equal 333, @box::ForConsts::CONST3 + + @box::EnvUtil.suppress_warning do + @box::ForConsts.const_set(:CONST3, 334) + end + assert_equal 334, @box::ForConsts::CONST3 + assert_equal 334, @box::ForConsts.refer3 + assert_equal 334, @box::ForConsts.get3 + + assert_equal 10, @box::ForConsts.refer_top_const + + # use Proxy object to use usual methods instead of singleton methods + proxy = @box::ForConsts::Proxy.new + + assert_raise(NameError){ proxy.call_str_refer0 } + assert_raise(NameError){ proxy.call_str_get0 } + + proxy.call_str_set0(30) + assert_equal 30, proxy.call_str_refer0 + assert_equal 30, proxy.call_str_get0 + assert_equal 999, String::STR_CONST0 + + proxy.call_str_remove0 + assert_raise(NameError){ proxy.call_str_refer0 } + assert_raise(NameError){ proxy.call_str_get0 } + + assert_equal 112, proxy.call_str_refer1 + assert_equal 112, proxy.call_str_get1 + assert_equal 223, proxy.call_str_refer2 + assert_equal 223, proxy.call_str_get2 + assert_equal 333, proxy.call_str_refer3 + assert_equal 333, proxy.call_str_get3 + + EnvUtil.suppress_warning do + proxy.call_str_set3 + end + assert_equal 334, proxy.call_str_refer3 + assert_equal 334, proxy.call_str_get3 + + assert_equal 1, proxy.refer_int_const1 + + assert_equal 999, String::STR_CONST0 + assert_raise(NameError) { String::STR_CONST1 } + assert_raise(NameError) { String::STR_CONST2 } + assert_raise(NameError) { String::STR_CONST3 } + assert_raise(NameError) { Integer::INT_CONST1 } + end + + def test_global_variables + default_l = $-0 + default_f = $, + + setup_box + + assert_equal "\n", $-0 # equal to $/, line splitter + assert_equal nil, $, # field splitter + + @box.require_relative('box/global_vars') + + # read first + assert_equal "\n", @box::LineSplitter.read + @box::LineSplitter.write("\r\n") + assert_equal "\r\n", @box::LineSplitter.read + assert_equal "\n", $-0 + + # write first + @box::FieldSplitter.write(",") + assert_equal ",", @box::FieldSplitter.read + assert_equal nil, $, + + # used only in box + assert_not_include? global_variables, :$used_only_in_box + @box::UniqueGvar.write(123) + assert_equal 123, @box::UniqueGvar.read + assert_nil $used_only_in_box + + # Kernel#global_variables returns the sum of all gvars. + global_gvars = global_variables.sort + assert_equal global_gvars, @box::UniqueGvar.gvars_in_box.sort + @box::UniqueGvar.write_only(456) + assert_equal (global_gvars + [:$write_only_var_in_box]).sort, @box::UniqueGvar.gvars_in_box.sort + assert_equal (global_gvars + [:$write_only_var_in_box]).sort, global_variables.sort + ensure + EnvUtil.suppress_warning do + $-0 = default_l + $, = default_f + end + end + + def test_match_variables_are_not_cached_in_box + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + /(?<a>foo)/ =~ 'bar' + /(?<b>baz)/ =~ 'baz' + assert_equal "baz", b + assert_equal "baz", $~.to_s + + /foo/ =~ 'bar' + assert_nil $~ + /(?<word>foo)(bar)?/ =~ 'foo' + assert_equal "foo", word + assert_equal "foo", $~.to_s + assert_equal "foo", $& + assert_equal "", $` + assert_equal "", $' + assert_equal "foo", $+ + end; + end + + def test_lastline_not_cached_in_box + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + r, w = IO.pipe + w.write("first\nsecond\n") + w.close + STDIN.reopen(r) + via_gets = Ruby::Box.new.eval(<<~'CODE') + gets + _ = $_ + gets + $_ + CODE + assert_equal "second\n", via_gets + end; + end + + def test_lastline_not_cached_in_nested_boxes + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + r, w = IO.pipe + w.write("outer1\ninner1\ninner2\nouter2\n") + w.close + STDIN.reopen(r) + inner_via_gets, outer_via_gets = Ruby::Box.new.eval(<<~'CODE') + gets + _ = $_ + + inner_result = Ruby::Box.new.eval(<<~'INNER') + gets + _ = $_ + gets + $_ + INNER + + gets + [inner_result, $_] + CODE + assert_equal "inner2\n", inner_via_gets + assert_equal "outer2\n", outer_via_gets + end; + end + + def test_errinfo_not_cached_in_box + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + first, second = Ruby::Box.new.eval(<<~'CODE') + a = begin; raise "first"; rescue RuntimeError; $!.message; end + b = begin; raise "second"; rescue RuntimeError; $!.message; end + [a, b] + CODE + assert_equal "first", first + assert_equal "second", second + end; + end + + def test_errinfo_not_cached_in_nested_boxes + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + inner_msg, outer_msg = Ruby::Box.new.eval(<<~'CODE') + outer_a = begin; raise "outer1"; rescue RuntimeError; $!.message; end + + inner_msg = Ruby::Box.new.eval(<<~'INNER') + begin; raise "inner1"; rescue RuntimeError; $!; end + begin; raise "inner2"; rescue RuntimeError; $!.message; end + INNER + + outer_b = begin; raise "outer2"; rescue RuntimeError; $!.message; end + [inner_msg, outer_b] + CODE + assert_equal "inner2", inner_msg + assert_equal "outer2", outer_msg + end; + end + + def test_backtrace_not_cached_in_box + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + a_actual, b_actual = Ruby::Box.new.eval(<<~'CODE') + a_actual = begin; raise "first"; rescue RuntimeError; $@.first[/:(\d+):/, 1].to_i; end + b_actual = begin; raise "second"; rescue RuntimeError; $@.first[/:(\d+):/, 1].to_i; end + [a_actual, b_actual] + CODE + assert_equal 1, a_actual + assert_equal 2, b_actual + end; + end + + def test_backtrace_not_cached_in_nested_boxes + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + inner_actual, outer_actual = Ruby::Box.new.eval(<<~'CODE') + begin; raise "outer1"; rescue RuntimeError; $@; end + inner_actual = Ruby::Box.new.eval(<<~'INNER') + begin; raise "inner1"; rescue RuntimeError; $@; end + begin; raise "inner2"; rescue RuntimeError; $@.first[/:(\d+):/, 1].to_i; end + INNER + outer_actual = begin; raise "outer2"; rescue RuntimeError; $@.first[/:(\d+):/, 1].to_i; end + [inner_actual, outer_actual] + CODE + assert_equal 2, inner_actual + assert_equal 6, outer_actual + end; + end + + def test_errinfo_isolated_between_boxes + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + box_a = Ruby::Box.new + box_b = Ruby::Box.new + + a = box_a.eval('begin; raise "a"; rescue; $!.message; end') + b = box_b.eval('begin; raise "b"; rescue; $!.message; end') + + assert_equal "a", a + assert_equal "b", b + end; + end + + def test_backtrace_isolated_between_boxes + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + box_a = Ruby::Box.new + box_b = Ruby::Box.new + + a_line = box_a.eval("\nbegin; raise; rescue; $@.first[/:(\\d+):/, 1].to_i; end") + b_line = box_b.eval('begin; raise; rescue; $@.first[/:(\d+):/, 1].to_i; end') + + assert_equal 2, a_line + assert_equal 1, b_line + end; + end + + def test_inner_box_rescue_does_not_disturb_outer_box_errinfo + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + box_a = Ruby::Box.new + errinfo_in_inner_rescue, errinfo_after_inner_rescue, errinfo_back_in_outer_rescue = box_a.eval(<<~'A') + errinfo_in_inner_rescue = errinfo_after_inner_rescue = errinfo_back_in_outer_rescue = nil + begin + raise "outer" + rescue + errinfo_in_inner_rescue, errinfo_after_inner_rescue = Ruby::Box.new.eval(<<~'B') + in_rescue = after_rescue = nil + begin + raise "inner" + rescue + in_rescue = $! && $!.message + end + after_rescue = $! && $!.message + [in_rescue, after_rescue] + B + errinfo_back_in_outer_rescue = $! && $!.message + end + [errinfo_in_inner_rescue, errinfo_after_inner_rescue, errinfo_back_in_outer_rescue] + A + + assert_equal "inner", errinfo_in_inner_rescue + assert_equal "outer", errinfo_after_inner_rescue + assert_equal "outer", errinfo_back_in_outer_rescue + end; + end + + def test_load_path_and_loaded_features + setup_box + + assert_respond_to $LOAD_PATH, :resolve_feature_path + + @box.require_relative('box/load_path') + + assert_not_equal $LOAD_PATH, @box::LoadPathCheck::FIRST_LOAD_PATH + + assert @box::LoadPathCheck::FIRST_LOAD_PATH_RESPOND_TO_RESOLVE + + box_dir = File.join(__dir__, 'box') + # TODO: $LOADED_FEATURES in method calls should refer the current box in addition to the loading box. + # assert_include @box::LoadPathCheck.current_loaded_features, File.join(box_dir, 'blank1.rb') + # assert_not_include @box::LoadPathCheck.current_loaded_features, File.join(box_dir, 'blank2.rb') + # assert_predicate @box::LoadPathCheck, :require_blank2 + # assert_include(@box::LoadPathCheck.current_loaded_features, File.join(box_dir, 'blank2.rb')) + + assert_not_include $LOADED_FEATURES, File.join(box_dir, 'blank1.rb') + assert_not_include $LOADED_FEATURES, File.join(box_dir, 'blank2.rb') + end + + def test_eval_basic + setup_box + + # Test basic evaluation + result = @box.eval("1 + 1") + assert_equal 2, result + + # Test string evaluation + result = @box.eval("'hello ' + 'world'") + assert_equal "hello world", result + end + + def test_eval_with_constants + setup_box + + # Define a constant in the box via eval + @box.eval("TEST_CONST = 42") + assert_equal 42, @box::TEST_CONST + + # Constant should not be visible in main box + assert_raise(NameError) { TEST_CONST } + end + + def test_eval_with_classes + setup_box + + # Define a class in the box via eval + @box.eval("class TestClass; def hello; 'from box'; end; end") + + # Class should be accessible in the box + instance = @box::TestClass.new + assert_equal "from box", instance.hello + + # Class should not be visible in main box + assert_raise(NameError) { TestClass } + end + + def test_eval_isolation + setup_box + + # Create another box + n2 = Ruby::Box.new + + # Define different constants in each box + @box.eval("ISOLATION_TEST = 'first'") + n2.eval("ISOLATION_TEST = 'second'") + + # Each box should have its own constant + assert_equal "first", @box::ISOLATION_TEST + assert_equal "second", n2::ISOLATION_TEST + + # Constants should not interfere with each other + assert_not_equal @box::ISOLATION_TEST, n2::ISOLATION_TEST + end + + def test_eval_with_variables + setup_box + + # Test local variable access (should work within the eval context) + result = @box.eval("x = 10; y = 20; x + y") + assert_equal 30, result + end + + def test_eval_error_handling + setup_box + + # Test syntax error + assert_raise(SyntaxError) { @box.eval("1 +") } + + # Test name error + assert_raise(NameError) { @box.eval("undefined_variable") } + + # Test that box is properly restored after error + begin + @box.eval("raise RuntimeError, 'test error'") + rescue RuntimeError + # Should be able to continue using the box + result = @box.eval("2 + 2") + assert_equal 4, result + end + end + + # Tests which run always (w/o RUBY_BOX=1 globally) + + def test_prelude_gems_and_loaded_features + assert_in_out_err([ENV_ENABLE_BOX, "--enable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join + puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join + + require "error_highlight" + + puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join + puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join + end; + + # No additional warnings except for experimental warnings + assert_equal 2, error.size + assert_match EXPERIMENTAL_WARNING_LINE_PATTERNS[0], error[0] + assert_match EXPERIMENTAL_WARNING_LINE_PATTERNS[1], error[1] + + assert_includes output.grep(/^before:/).join("\n"), '/bundled_gems.rb' + assert_includes output.grep(/^before:/).join("\n"), '/error_highlight.rb' + assert_includes output.grep(/^after:/).join("\n"), '/bundled_gems.rb' + assert_includes output.grep(/^after:/).join("\n"), '/error_highlight.rb' + end + end + + def test_prelude_gems_and_loaded_features_with_disable_gems + assert_in_out_err([ENV_ENABLE_BOX, "--disable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join + puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join + + require "error_highlight" + + puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join + puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join + end; + + assert_equal 2, error.size + assert_match EXPERIMENTAL_WARNING_LINE_PATTERNS[0], error[0] + assert_match EXPERIMENTAL_WARNING_LINE_PATTERNS[1], error[1] + + refute_includes output.grep(/^before:/).join("\n"), '/bundled_gems.rb' + refute_includes output.grep(/^before:/).join("\n"), '/error_highlight.rb' + refute_includes output.grep(/^after:/).join("\n"), '/bundled_gems.rb' + assert_includes output.grep(/^after:/).join("\n"), '/error_highlight.rb' + end + end + + def test_calling_root_box_methods_does_not_change_user_boxes_newly_created + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true, timeout: 60) + begin; + assert_not_include Object.constants.sort, :Find # required by Pathname#find + assert_not_include Ruby::Box.root.eval("Object.constants.sort"), :Find + b1 = Ruby::Box.new + assert_not_include b1.eval("Object.constants.sort"), :Find + + require 'pathname' + Pathname.new('.').find{|path| path.directory?} + assert_include Object.constants.sort, :Find # required by Pathname#find + + assert_not_include Ruby::Box.root.eval("Object.constants.sort"), :Find + assert_not_include b1.eval("Object.constants.sort"), :Find + + Ruby::Box.root.eval("require 'pathname'; Pathname.new('.').find{|path| path.directory? }") + assert_include Ruby::Box.root.eval("Object.constants.sort"), :Find + + assert_not_include b1.eval("Object.constants.sort"), :Find + b2 = Ruby::Box.new + assert_not_include b2.eval("Object.constants.sort"), :Find + end; + end + + def test_boxes_have_different_rubygems + # assert_separately w/ ENV_ENABLE_BOX and --enable=gems causes timeouts on CI @ Windows + assert_in_out_err([ENV_ENABLE_BOX, "--enable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + require "json" + h = {main: Gem.object_id, root: Ruby::Box.root.eval("Gem").object_id, box: Ruby::Box.new.eval("Gem").object_id} + puts h.to_json + end; + require "json" + result = JSON.parse(output.first, symbolize_names: true) + assert_not_equal result[:main], result[:root] + assert_not_equal result[:box], result[:root] + assert_not_equal result[:main], result[:box] + end + end + + def test_require_list_loaded_only_in_main_box + Tempfile.create(["req_a", ".rb"]) do |t1| + Tempfile.create(["req_b", ".rb"]) do |t2| + t1.puts "module FooBarA; end" + t1.close + t2.puts "module FooBarB; end" + t2.close + + opts = [ENV_ENABLE_BOX, "-r#{t1.path}", "-r#{t2.path}"] + assert_separately(opts, __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + main_constants = Object.constants + assert_include main_constants, :FooBarA + assert_include main_constants, :FooBarB + + root_constants = Ruby::Box.root.eval("Object.constants.sort") + master_constants = Ruby::Box.master.eval("Object.constants.sort") + assert_not_include root_constants, :FooBarA + assert_not_include root_constants, :FooBarB + assert_not_include master_constants, :FooBarA + assert_not_include master_constants, :FooBarB + end; + end + end + end + + def test_root_and_main_methods + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + pend unless Ruby::Box.respond_to?(:root) and Ruby::Box.respond_to?(:main) # for RUBY_DEBUG > 0 + + assert_respond_to Ruby::Box.root, :root? + assert_respond_to Ruby::Box.main, :main? + + assert_predicate Ruby::Box.root, :root? + assert_predicate Ruby::Box.main, :main? + assert_equal Ruby::Box.main, Ruby::Box.current + + $a = 1 + $LOADED_FEATURES.push("/tmp/foobar") + + assert_equal 2, Ruby::Box.root.eval('$a = 2; $a') + assert_not_include Ruby::Box.root.eval('$LOADED_FEATURES.push("/tmp/barbaz"); $LOADED_FEATURES'), "/tmp/foobar" + assert_equal "FooClass", Ruby::Box.root.eval('class FooClass; end; Object.const_get(:FooClass).to_s') + + assert_equal 1, $a + assert_not_include $LOADED_FEATURES, "/tmp/barbaz" + assert_not_operator Object, :const_defined?, :FooClass + end; + end + + def test_basic_box_detections + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + box = Ruby::Box.new + $gvar1 = 'bar' + code = <<~EOC + BOX1 = Ruby::Box.current + $gvar1 = 'foo' + + def toplevel = $gvar1 + + class Foo + BOX2 = Ruby::Box.current + BOX2_proc = ->(){ BOX2 } + BOX3_proc = ->(){ Ruby::Box.current } + + def box4 = Ruby::Box.current + def self.box5 = BOX2 + def self.box6 = Ruby::Box.current + def self.box6_proc = ->(){ Ruby::Box.current } + def self.box7 + res = [] + [1,2].chunk{ it.even? }.each do |bool, members| + res << Ruby::Box.current.object_id.to_s + ":" + bool.to_s + ":" + members.map(&:to_s).join(",") + end + res + end + + def self.yield_block = yield + def self.call_block(&b) = b.call + + def self.gvar1 = $gvar1 + def self.call_toplevel = toplevel + end + FOO_NAME = Foo.name + + module Kernel + def foo_box = Ruby::Box.current + module_function :foo_box + end + + BOX_X = Foo.new.box4 + BOX_Y = foo_box + EOC + box.eval(code) + outer = Ruby::Box.current + assert_equal box, box::BOX1 # on TOP frame + assert_equal box, box::Foo::BOX2 # on CLASS frame + assert_equal box, box::Foo::BOX2_proc.call # proc -> a const on CLASS + assert_equal box, box::Foo::BOX3_proc.call # proc -> the current + assert_equal box, box::Foo.new.box4 # instance method -> the current + assert_equal box, box::Foo.box5 # singleton method -> a const on CLASS + assert_equal box, box::Foo.box6 # singleton method -> the current + assert_equal box, box::Foo.box6_proc.call # method returns a proc -> the current + + # a block after CFUNC/IFUNC in a method -> the current + assert_equal ["#{box.object_id}:false:1", "#{box.object_id}:true:2"], box::Foo.box7 + + assert_equal outer, box::Foo.yield_block{ Ruby::Box.current } # method yields + assert_equal outer, box::Foo.call_block{ Ruby::Box.current } # method calls a block + + assert_equal 'foo', box::Foo.gvar1 # method refers gvar + assert_equal 'bar', $gvar1 # gvar value out of the box + assert_equal 'foo', box::Foo.call_toplevel # toplevel method referring gvar + + assert_equal box, box::BOX_X # on TOP frame, referring a class in the current + assert_equal box, box::BOX_Y # on TOP frame, referring Kernel method defined by a CFUNC method + + assert_equal "Foo", box::FOO_NAME + assert_equal "Foo", box::Foo.name + end; + end + + def test_very_basic_method_calls_and_constants + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + code = <<~EOC + consts = Object.constants + [consts.include?(:String), consts.include?(:Array)] + EOC + assert_equal([true, true], Ruby::Box.current.eval(code)) + assert_equal([true, true], Ruby::Box.root.eval(code)) + end; + end + + def test_loading_extension_libs_in_main_box_1 + pend if /mswin|mingw/ =~ RUBY_PLATFORM # timeout on windows environments + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + require "prism" + require "optparse" + require "date" + require "time" + require "delegate" + require "singleton" + require "pp" + require "fileutils" + require "tempfile" + require "tmpdir" + require "json" + require "psych" + require "yaml" + expected = 1 + assert_equal expected, 1 + end; + end + + def test_loading_extension_libs_in_main_box_2 + pend if /mswin|mingw/ =~ RUBY_PLATFORM # timeout on windows environments + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + require "zlib" + require "open3" + require "ipaddr" + require "net/http" + require "openssl" + require "socket" + require "uri" + require "digest" + require "erb" + require "stringio" + require "monitor" + require "timeout" + require "securerandom" + expected = 1 + assert_equal expected, 1 + end; + end + + def test_mark_box_object_referred_only_from_binding + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + box = Ruby::Box.new + box.eval('class Integer; def +(*)=42; end') + b = box.eval('binding') + box = nil # remove direct reference to the box + + assert_equal 42, b.eval('1+2') + + GC.stress = true + GC.start + + assert_equal 42, b.eval('1+2') + end; + end + + def test_loaded_extension_deleted_in_user_box + require 'tmpdir' + Dir.mktmpdir do |tmpdir| + env = ENV_ENABLE_BOX.merge({'TMPDIR'=>tmpdir}) + assert_ruby_status([env], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + require "json" + end; + assert_empty(Dir.children(tmpdir)) + end + end + + def test_root_box_iclasses_should_be_boxable + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + Ruby::Box.root.eval("class IMath; include Math; end") # (*) + module Math + def foo = :foo + end + # This test crashes here if iclasses (created at the line (*) is not boxable) + class IMath2; include Math; end + assert_equal :foo, IMath2.new.foo + assert_raise NoMethodError do + Ruby::Box.root.eval("IMath.new.foo") + end + end; + end + + def test_user_box_iclass_with_module_modified_in_another_box + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + # A user box creates a class that includes a core module. + # The ICLASS is allocated in the user box context (non-boxable). + box1 = Ruby::Box.new + box1.eval("class IMath; include Math; end") + + # A second user box adds an instance method on that module, + # triggering classext duplication which iterates the module's + # subclass list and encounters box1's non-boxable ICLASS. + box2 = Ruby::Box.new + box2.eval("module Math; def box2_test = :box2; end") + + assert_equal :box2, box2.eval("Class.new { include Math }.new.box2_test") + end; + end + + def test_method_invalidation_between_boxes_1 + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + b = Ruby::Box.new + b.eval(<<~'RUBY') + Module.prepend(Module.new) + class C; end + class D < C; end + def C.===(x) = true + RUBY + + assert String === "x" + assert b # to prevent GCing b + end; + end + + def test_method_invalidation_between_boxes_2 + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + PrepM = Module.new + Module.prepend(PrepM) + Module.new.include?(Module.new) + + b = Ruby::Box.new + b.eval(<<~'RUBY') + Module.class_eval { def _test_method; end } + + class C; end + class D < C; end + def C.include?(x) = true + RUBY + + Module.new.include?(Module.new) + end; + end +end diff --git a/test/ruby/test_call.rb b/test/ruby/test_call.rb index 7843f3b476..dd1936c4e2 100644 --- a/test/ruby/test_call.rb +++ b/test/ruby/test_call.rb @@ -123,6 +123,25 @@ class TestCall < Test::Unit::TestCase assert_equal([1, 2, {kw: 3}], f(*a, kw: 3)) end + def test_forward_argument_init + o = Object.new + def o.simple_forward_argument_init(a=eval('b'), b=1) + [a, b] + end + + def o.complex_forward_argument_init(a=eval('b'), b=eval('kw'), kw: eval('kw2'), kw2: 3) + [a, b, kw, kw2] + end + + def o.keyword_forward_argument_init(a: eval('b'), b: eval('kw'), kw: eval('kw2'), kw2: 3) + [a, b, kw, kw2] + end + + assert_equal [nil, 1], o.simple_forward_argument_init + assert_equal [nil, nil, 3, 3], o.complex_forward_argument_init + assert_equal [nil, nil, 3, 3], o.keyword_forward_argument_init + end + def test_call_bmethod_proc pr = proc{|sym| sym} define_singleton_method(:a, &pr) @@ -423,6 +442,35 @@ class TestCall < Test::Unit::TestCase assert_equal([1, 2, {}], s(*r2ka)) end + def test_anon_splat_mutated_bug_21757 + args = [1, 2] + kw = {bug: true} + + def self.m(*); end + m(*args, bug: true) + assert_equal(2, args.length) + + proc = ->(*) { } + proc.(*args, bug: true) + assert_equal(2, args.length) + + def self.m2(*); end + m2(*args, **kw) + assert_equal(2, args.length) + + proc = ->(*) { } + proc.(*args, **kw) + assert_equal(2, args.length) + + def self.m3(*, **nil); end + assert_raise(ArgumentError) { m3(*args, bug: true) } + assert_equal(2, args.length) + + proc = ->(*, **nil) { } + assert_raise(ArgumentError) { proc.(*args, bug: true) } + assert_equal(2, args.length) + end + def test_kwsplat_block_eval_order def self.t(**kw, &b) [kw, b] end diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb index 74541bba3f..82199876ec 100644 --- a/test/ruby/test_class.rb +++ b/test/ruby/test_class.rb @@ -259,6 +259,46 @@ class TestClass < Test::Unit::TestCase assert_raise(TypeError) { BasicObject.dup } end + def test_class_hierarchy_inside_initialize_dup_bug_21538 + ancestors = sc_ancestors = nil + b = Class.new + b.define_singleton_method(:initialize_dup) do |x| + ancestors = self.ancestors + sc_ancestors = singleton_class.ancestors + super(x) + end + + a = Class.new(b) + + c = a.dup + + expected_ancestors = [c, b, *Object.ancestors] + expected_sc_ancestors = [c.singleton_class, b.singleton_class, *Object.singleton_class.ancestors] + assert_equal expected_ancestors, ancestors + assert_equal expected_sc_ancestors, sc_ancestors + assert_equal expected_ancestors, c.ancestors + assert_equal expected_sc_ancestors, c.singleton_class.ancestors + end + + def test_class_hierarchy_inside_initialize_clone_bug_21538 + ancestors = sc_ancestors = nil + a = Class.new + a.define_singleton_method(:initialize_clone) do |x| + ancestors = self.ancestors + sc_ancestors = singleton_class.ancestors + super(x) + end + + c = a.clone + + expected_ancestors = [c, *Object.ancestors] + expected_sc_ancestors = [c.singleton_class, *Object.singleton_class.ancestors] + assert_equal expected_ancestors, ancestors + assert_equal expected_sc_ancestors, sc_ancestors + assert_equal expected_ancestors, c.ancestors + assert_equal expected_sc_ancestors, c.singleton_class.ancestors + end + def test_singleton_class assert_raise(TypeError) { 1.extend(Module.new) } assert_raise(TypeError) { 1.0.extend(Module.new) } @@ -395,21 +435,24 @@ class TestClass < Test::Unit::TestCase end class CloneTest + TEST = :C0 def foo; TEST; end end CloneTest1 = CloneTest.clone CloneTest2 = CloneTest.clone class CloneTest1 + remove_const :TEST TEST = :C1 end class CloneTest2 + remove_const :TEST TEST = :C2 end def test_constant_access_from_method_in_cloned_class - assert_equal :C1, CloneTest1.new.foo, '[Bug #15877]' - assert_equal :C2, CloneTest2.new.foo, '[Bug #15877]' + assert_equal :C0, CloneTest1.new.foo, 'originally [Bug #15877], but behaviour changed' + assert_equal :C0, CloneTest2.new.foo, 'originally [Bug #15877], but behaviour changed' end def test_invalid_superclass @@ -561,7 +604,7 @@ class TestClass < Test::Unit::TestCase obj = Object.new c = obj.singleton_class obj.freeze - assert_raise_with_message(FrozenError, /frozen object/) { + assert_raise_with_message(FrozenError, /frozen Object/) { c.class_eval {def f; end} } end @@ -693,6 +736,29 @@ class TestClass < Test::Unit::TestCase } end + def test_dynamic_module_cpath_constant_namespace # [Bug #20948] + assert_separately([], <<~'RUBY') + module M1 + module Foo + X = 1 + end + end + + module M2 + module Foo + X = 2 + end + end + + results = [M1, M2].map do + module it::Foo + X + end + end + assert_equal([1, 2], results) + RUBY + end + def test_namescope_error_message m = Module.new o = m.module_eval "class A\u{3042}; self; end.new" @@ -847,4 +913,72 @@ CODE class C; end end; end + + def test_define_singleton_initialize + assert_normal_exit "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + class C + def self.initialize + end + end + end; + end + + def test_singleton_cc_invalidation + assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}") + begin; + class T + def hi + "hi" + end + end + + t = T.new + t.singleton_class + + def hello(t) + t.hi + end + + 5.times do + hello(t) # populate inline cache on `t.singleton_class`. + end + + class T + remove_method :hi # invalidate `t.singleton_class` ccs for `hi` + end + + assert_raise NoMethodError do + hello(t) + end + end; + end + + def test_safe_multi_ractor_subclasses_list_mutation + assert_ractor "#{<<~"begin;"}\n#{<<~'end;'}", signal: :SEGV + begin; + 4.times.map do + Ractor.new do + 20_000.times do + Object.new.singleton_class + end + end + end.each(&:join) + end; + end + + def test_safe_multi_ractor_singleton_class_access + assert_ractor "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + class A; end + 4.times.map do + Ractor.new do + a = A + 100.times do + a = a.singleton_class + end + end + end.each(&:join) + end; + end end diff --git a/test/ruby/test_compile_prism.rb b/test/ruby/test_compile_prism.rb index b95add5bd4..c017111c0a 100644 --- a/test/ruby/test_compile_prism.rb +++ b/test/ruby/test_compile_prism.rb @@ -1046,16 +1046,19 @@ module Prism end def test_ForNode - assert_prism_eval("for i in [1,2] do; i; end") - assert_prism_eval("for @i in [1,2] do; @i; end") - assert_prism_eval("for $i in [1,2] do; $i; end") + assert_prism_eval("r = []; for i in [1,2] do; r << i; end; r") + assert_prism_eval("r = []; for @i in [1,2] do; r << @i; end; r") + assert_prism_eval("r = []; for $i in [1,2] do; r << $i; end; r") - assert_prism_eval("for foo, in [1,2,3] do end") + assert_prism_eval("r = []; for foo, in [1,2,3] do r << foo end; r") - assert_prism_eval("for i, j in {a: 'b'} do; i; j; end") + assert_prism_eval("r = []; for i, j in {a: 'b'} do; r << [i, j]; end; r") # Test splat node as index in for loop - assert_prism_eval("for *x in [[1,2], [3,4]] do; x; end") + assert_prism_eval("r = []; for *x in [[1,2], [3,4]] do; r << x; end; r") + assert_prism_eval("r = []; for * in [[1,2], [3,4]] do; r << 'ok'; end; r") + assert_prism_eval("r = []; for x, * in [[1,2], [3,4]] do; r << x; end; r") + assert_prism_eval("r = []; for x, *y in [[1,2], [3,4]] do; r << [x, y]; end; r") end ############################################################################ @@ -2691,7 +2694,7 @@ end # Errors # ############################################################################ - def test_MissingNode + def test_ErrorRecoveryNode # TODO end @@ -2718,6 +2721,12 @@ end assert_raise TypeError do RubyVM::InstructionSequence.compile_file_prism(nil) end + + assert_nothing_raised(Errno::EMFILE, Errno::ENFILE) do + 10000.times do + RubyVM::InstructionSequence.compile_file_prism(File::NULL) + end + end end private diff --git a/test/ruby/test_data.rb b/test/ruby/test_data.rb index dd698fdcc4..4818c8acb7 100644 --- a/test/ruby/test_data.rb +++ b/test/ruby/test_data.rb @@ -69,15 +69,33 @@ class TestData < Test::Unit::TestCase assert_equal(1, test_kw.foo) assert_equal(2, test_kw.bar) assert_equal(test_kw, klass.new(foo: 1, bar: 2)) + assert_equal(test_kw, klass.new('foo' => 1, 'bar' => 2)) assert_equal(test_kw, test) # Wrong protocol assert_raise(ArgumentError) { klass.new(1) } assert_raise(ArgumentError) { klass.new(1, 2, 3) } - assert_raise(ArgumentError) { klass.new(foo: 1) } - assert_raise(ArgumentError) { klass.new(foo: 1, bar: 2, baz: 3) } - # Could be converted to foo: 1, bar: 2, but too smart is confusing - assert_raise(ArgumentError) { klass.new(1, bar: 2) } + assert_raise(TypeError) do + klass.new(0 => 1, 1 => 2) + end + assert_raise(TypeError) do + klass.new(foo: 0, bar: 2, 0 => 1) + end + assert_raise_with_message(ArgumentError, "missing keyword: :bar") do + klass.new(foo: 1) + end + assert_raise_with_message(ArgumentError, "missing keyword: :bar") do + klass.new('foo' => 1) + end + assert_raise_with_message(ArgumentError, "missing keyword: :bar") do + klass.new(foo: 1, 'foo' => 1) + end + assert_raise_with_message(ArgumentError, "missing keywords: :foo, :bar") do + klass.new(x: 1, y: 2) + end + assert_raise_with_message(ArgumentError, "unknown keyword: :baz") do + klass.new(foo: 1, bar: 2, baz: 3) + end end def test_initialize_redefine @@ -259,9 +277,10 @@ class TestData < Test::Unit::TestCase assert_equal(klass.new, test) assert_not_equal(Data.define.new, test) - assert_equal('#<data >', test.inspect) + assert_equal('#<data>', test.inspect) assert_equal([], test.members) assert_equal({}, test.to_h) + assert_predicate(test, :frozen?) end def test_dup diff --git a/test/ruby/test_defined.rb b/test/ruby/test_defined.rb index db1fdc8e25..75ed1a7534 100644 --- a/test/ruby/test_defined.rb +++ b/test/ruby/test_defined.rb @@ -62,6 +62,34 @@ class TestDefined < Test::Unit::TestCase f.bar(Class.new(Foo).new) { |v| assert(v, "inherited protected method") } end + module ProtectedInModule + def m + :m + end + protected :m + def call_m(o) + o.m + end + def defined_m(o) + defined?(o.m) + end + end + class ProtectedIncluderA + include ProtectedInModule + end + class ProtectedIncluderB + include ProtectedInModule + end + + def test_defined_protected_method_in_included_module + a = ProtectedIncluderA.new + b = ProtectedIncluderB.new + assert_equal(:m, a.call_m(a)) + assert_equal(:m, a.call_m(b)) + assert_equal("method", a.defined_m(a)) + assert_equal("method", a.defined_m(b)) + end + def test_defined_undefined_method f = Foo.new assert_nil(defined?(f.quux)) # undefined method diff --git a/test/ruby/test_encoding.rb b/test/ruby/test_encoding.rb index 5c1eb50bb1..0cd5bf49dc 100644 --- a/test/ruby/test_encoding.rb +++ b/test/ruby/test_encoding.rb @@ -154,7 +154,7 @@ class TestEncoding < Test::Unit::TestCase r, _obj = Ractor.select(*rs) rs.delete(r) end - assert rs.empty? + assert_empty rs end; end @@ -173,7 +173,7 @@ class TestEncoding < Test::Unit::TestCase r, _obj = Ractor.select(*rs) rs.delete(r) end - assert rs.empty? + assert_empty rs end; end end diff --git a/test/ruby/test_enum.rb b/test/ruby/test_enum.rb index 237bdc8a4d..32ec4f5779 100644 --- a/test/ruby/test_enum.rb +++ b/test/ruby/test_enum.rb @@ -69,11 +69,11 @@ class TestEnumerable < Test::Unit::TestCase assert_equal(['z', 42, nil], [:a, 'b', 'z', :c, 42, nil].grep_v(/[a-d]/), bug17030) assert_equal('match', $1, bug17030) - regexp = Regexp.new('x') - assert_equal([], @obj.grep(regexp), bug17030) # sanity check - def regexp.===(other) - true - end + regexp = Class.new(Regexp) { + def ===(other) + true + end + }.new('x') assert_equal([1, 2, 3, 1, 2], @obj.grep(regexp), bug17030) o = Object.new diff --git a/test/ruby/test_enumerator.rb b/test/ruby/test_enumerator.rb index cd62cd8acb..9b972d7b22 100644 --- a/test/ruby/test_enumerator.rb +++ b/test/ruby/test_enumerator.rb @@ -886,6 +886,7 @@ class TestEnumerator < Test::Unit::TestCase def test_produce assert_raise(ArgumentError) { Enumerator.produce } + assert_raise(ArgumentError) { Enumerator.produce(a: 1, b: 1) {} } # Without initial object passed_args = [] @@ -903,14 +904,6 @@ class TestEnumerator < Test::Unit::TestCase assert_equal [1, 2, 3], enum.take(3) assert_equal [1, 2], passed_args - # With initial keyword arguments - passed_args = [] - enum = Enumerator.produce(a: 1, b: 1) { |obj| passed_args << obj; obj.shift if obj.respond_to?(:shift)} - assert_instance_of(Enumerator, enum) - assert_equal Float::INFINITY, enum.size - assert_equal [{b: 1}, [1], :a, nil], enum.take(4) - assert_equal [{b: 1}, [1], :a], passed_args - # Raising StopIteration words = "The quick brown fox jumps over the lazy dog.".scan(/\w+/) enum = Enumerator.produce { words.shift or raise StopIteration } @@ -935,6 +928,25 @@ class TestEnumerator < Test::Unit::TestCase "abc", ], enum.to_a } + + # With size keyword argument + enum = Enumerator.produce(1, size: 10) { |obj| obj.succ } + assert_equal 10, enum.size + assert_equal [1, 2, 3], enum.take(3) + + enum = Enumerator.produce(1, size: -> { 5 }) { |obj| obj.succ } + assert_equal 5, enum.size + + enum = Enumerator.produce(1, size: nil) { |obj| obj.succ } + assert_equal nil, enum.size + + enum = Enumerator.produce(1, size: Float::INFINITY) { |obj| obj.succ } + assert_equal Float::INFINITY, enum.size + + # Without initial value but with size + enum = Enumerator.produce(size: 3) { |obj| (obj || 0).succ } + assert_equal 3, enum.size + assert_equal [1, 2, 3], enum.take(3) end def test_chain_each_lambda diff --git a/test/ruby/test_env.rb b/test/ruby/test_env.rb index 2727620c19..dd526544af 100644 --- a/test/ruby/test_env.rb +++ b/test/ruby/test_env.rb @@ -281,6 +281,26 @@ class TestEnv < Test::Unit::TestCase assert_equal(["foo", "foo"], ENV.values_at("test", "test")) end + def test_fetch_values + ENV["test"] = "foo" + ENV["test2"] = "bar" + assert_equal(["foo", "bar"], ENV.fetch_values("test", "test2")) + assert_equal(["foo", "foo"], ENV.fetch_values("test", "test")) + assert_equal([], ENV.fetch_values) + + ENV.delete("test2") + assert_raise(KeyError) { ENV.fetch_values("test", "test2") } + + assert_equal(["foo", "default"], ENV.fetch_values("test", "test2") { "default" }) + assert_equal(["foo", "TEST2"], ENV.fetch_values("test", "test2") { |k| k.upcase }) + + e = assert_raise(KeyError) { ENV.fetch_values("test2") } + assert_same(ENV, e.receiver) + assert_equal("test2", e.key) + + assert_invalid_env {|v| ENV.fetch_values(v)} + end + def test_select ENV["test"] = "foo" h = ENV.select {|k| IGNORE_CASE ? k.upcase == "TEST" : k == "test" } @@ -1379,23 +1399,24 @@ class TestEnv < Test::Unit::TestCase Ractor.new port = Ractor::Port.new do |port| ENV["#{PATH_ENV}"] = "/" ENV.each do |k, v| - port.send [k.frozen?] - port.send [v.frozen?] + port.send [k] + port.send [v] end ENV.each_key do |k| - port.send [k.frozen?] + port.send [k] end ENV.each_value do |v| - port.send [v.frozen?] + port.send [v] end ENV.each_key do |k| - port.send [ENV[k].frozen?, "[\#{k.dump}]"] - port.send [ENV.fetch(k).frozen?, "fetch(\#{k.dump})"] + port.send [ENV[k], "[\#{k.dump}]"] + port.send [ENV.fetch(k), "fetch(\#{k.dump})"] end port.send "finished" end while((params=port.receive) != "finished") - assert(*params) + value, *params = params + assert_predicate(value, :frozen?, *params) end end; end diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index 84581180b6..4365150a13 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -992,7 +992,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| assert_equal 1, outs.size assert_equal 0, errs.size err = outs.first.force_encoding('utf-8') - assert err.valid_encoding?, 'must be valid encoding' + assert_predicate err, :valid_encoding? assert_match %r/\u3042/, err end end @@ -1525,4 +1525,31 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| assert_in_out_err(%W[-r#{lib} #{main}], "", [], [:*, "\n""path=#{main}\n", :*]) end end + + class Ex; end + + def test_exception_message_for_unexpected_implicit_conversion_type + a = Ex.new + def self.x(a) = nil + + assert_raise_with_message(TypeError, "no implicit conversion of TestException::Ex into Hash") do + x(**a) + end + assert_raise_with_message(TypeError, "no implicit conversion of TestException::Ex into Proc") do + x(&a) + end + + def a.to_a = 1 + def a.to_hash = 1 + def a.to_proc = 1 + assert_raise_with_message(TypeError, "can't convert TestException::Ex into Array (TestException::Ex#to_a gives Integer)") do + x(*a) + end + assert_raise_with_message(TypeError, "can't convert TestException::Ex into Hash (TestException::Ex#to_hash gives Integer)") do + x(**a) + end + assert_raise_with_message(TypeError, "can't convert TestException::Ex into Proc (TestException::Ex#to_proc gives Integer)") do + x(&a) + end + end end diff --git a/test/ruby/test_fiber.rb b/test/ruby/test_fiber.rb index b7d2b71c19..6976bd9742 100644 --- a/test/ruby/test_fiber.rb +++ b/test/ruby/test_fiber.rb @@ -506,4 +506,45 @@ class TestFiber < Test::Unit::TestCase GC.start RUBY end + + def test_fiber_pool_stack_acquire_failure + environment = { + "RUBY_SHARED_FIBER_POOL_MINIMUM_COUNT" => "0", + "RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT" => "128" + } + + # This program requires, effectively, at most one fiber stack, since the fiber immediately becomes unreachable. + assert_separately([environment], <<~RUBY, timeout: 30) + GC.disable + count_before = GC.count + + # Create more fibers than the pool can handle (but they become immediately unreachable): + assert_nothing_raised do + 256.times do + Fiber.new{Fiber.yield}.resume + end + end + + # Major GC should have happened at least once: + assert_operator(GC.count, :>, count_before) + RUBY + end + + def test_fiber_pool_stack_acquire_failure_at_maximum_count + environment = { + "RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT" => "128" + } + + assert_separately([environment], <<~RUBY, timeout: 30) + GC.disable + fibers = [] + assert_raise(FiberError) do + loop do + Fiber.new{fibers << Fiber.current; Fiber.yield}.resume + raise "expected FiberError before this" if fibers.size > 128 + end + end + assert_operator fibers.size, :>=, 128 + RUBY + end end diff --git a/test/ruby/test_file_exhaustive.rb b/test/ruby/test_file_exhaustive.rb index b20b597256..6e7973897c 100644 --- a/test/ruby/test_file_exhaustive.rb +++ b/test/ruby/test_file_exhaustive.rb @@ -197,12 +197,32 @@ class TestFileExhaustive < Test::Unit::TestCase [regular_file, utf8_file].each do |file| assert_equal(file, File.open(file) {|f| f.path}) assert_equal(file, File.path(file)) - o = Object.new - class << o; self; end.class_eval do - define_method(:to_path) { file } - end + o = Struct.new(:to_path).new(file) + assert_equal(file, File.path(o)) + o = Struct.new(:to_str).new(file) assert_equal(file, File.path(o)) end + + conv_error = ->(method, msg = "converting with #{method}") { + test = ->(&new) do + o = new.(42) + assert_raise(TypeError, msg) {File.path(o)} + + o = new.("abc".encode(Encoding::UTF_32BE)) + assert_raise(Encoding::CompatibilityError, msg) {File.path(o)} + + ["\0", "a\0", "a\0c"].each do |path| + o = new.(path) + assert_raise(ArgumentError, msg) {File.path(o)} + end + end + + test.call(&:itself) + test.call(&Struct.new(method).method(:new)) + } + + conv_error[:to_path] + conv_error[:to_str] end def assert_integer(n) @@ -877,10 +897,12 @@ class TestFileExhaustive < Test::Unit::TestCase bug9934 = '[ruby-core:63114] [Bug #9934]' require "objspace" path = File.expand_path("/foo") - assert_operator(ObjectSpace.memsize_of(path), :<=, path.bytesize + GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE], bug9934) + slot_size = Integer(ObjectSpace.dump(path)[/"slot_size":(\d+)/, 1]) + assert_operator(ObjectSpace.memsize_of(path), :<=, path.bytesize + slot_size, bug9934) path = File.expand_path("/a"*25) + slot_size = Integer(ObjectSpace.dump(path)[/"slot_size":(\d+)/, 1]) assert_operator(ObjectSpace.memsize_of(path), :<=, - (path.bytesize + 1) * 2 + GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE], bug9934) + (path.bytesize + 1) * 2 + slot_size, bug9934) end def test_expand_path_encoding @@ -1215,6 +1237,7 @@ class TestFileExhaustive < Test::Unit::TestCase assert_equal("foo", File.basename("foo", ".ext")) assert_equal("foo", File.basename("foo.ext", ".ext")) assert_equal("foo", File.basename("foo.ext", ".*")) + assert_raise(ArgumentError) {File.basename("", "\0")} end if NTFS @@ -1337,14 +1360,19 @@ class TestFileExhaustive < Test::Unit::TestCase end def test_join - s = "foo" + File::SEPARATOR + "bar" + File::SEPARATOR + "baz" + sep = File::SEPARATOR + s = "foo" + sep + "bar" + sep + "baz" assert_equal(s, File.join("foo", "bar", "baz")) assert_equal(s, File.join(["foo", "bar", "baz"])) + assert_equal(s, File.join("foo" + sep, "bar", sep + "baz")) + assert_equal(s, File.join("foo" + sep, sep + "bar" + sep, sep + "baz")) o = Object.new def o.to_path; "foo"; end assert_equal(s, File.join(o, "bar", "baz")) - assert_equal(s, File.join("foo" + File::SEPARATOR, "bar", File::SEPARATOR + "baz")) + + s = sep + "foo" + assert_equal(s, File.join(sep, s)) end def test_join_alt_separator diff --git a/test/ruby/test_float.rb b/test/ruby/test_float.rb index d0d180593a..c01e8bb80b 100644 --- a/test/ruby/test_float.rb +++ b/test/ruby/test_float.rb @@ -492,6 +492,22 @@ class TestFloat < Test::Unit::TestCase assert_equal(-1.26, -1.255.round(2)) end + def test_round_ndigits + bug14635 = "[ruby-core:86323]" + f = 0.5 + 31.times do |i| + assert_equal(0.5, f.round(i+1), bug14635 + " (argument: #{i+1})") + end + end + + def test_round_with_precision_min + (0..3).each do |n| + n -= Float::MIN_10_EXP + f = Float::MIN.round(n) + assert_include([Float::MIN.floor(n), Float::MIN.ceil(n)], f, "round(#{n})") + end + end + def test_round_half_even_with_precision assert_equal(767573.18759, 767573.1875850001.round(5, half: :even)) assert_equal(767573.18758, 767573.187585.round(5, half: :even)) @@ -536,6 +552,16 @@ class TestFloat < Test::Unit::TestCase assert_equal(-100000000000000000000000000000000000000000000000000, -1.0.floor(-50), "[Bug #20654]") end + def test_floor_with_precision_min + min = Float::MIN + (0..3).each do |n| + n -= Float::MIN_10_EXP + f = min.floor(n) + assert_operator(f, :<=, Float::MIN, "floor(#{n})") + assert_operator(f, :>=, Float::MIN.floor(n-1), "ceil(#{n})") + end + end + def test_ceil_with_precision assert_equal(+0.1, +0.001.ceil(1)) assert_equal(-0.0, -0.001.ceil(1)) @@ -567,6 +593,19 @@ class TestFloat < Test::Unit::TestCase assert_equal(100000000000000000000000000000000000000000000000000, 1.0.ceil(-50), "[Bug #20654]") end + def test_ceil_with_precision_min + min = Float::MIN + (-Float::MIN_10_EXP).times do |n| + assert_equal(10.pow(-n), min.ceil(n)) + end + (0..3).each do |n| + n -= Float::MIN_10_EXP + f = min.ceil(n) + assert_operator(f, :>=, Float::MIN, "ceil(#{n})") + assert_operator(f, :<=, Float::MIN.ceil(n-1), "ceil(#{n})") + end + end + def test_truncate_with_precision assert_equal(1.100, 1.111.truncate(1)) assert_equal(1.110, 1.111.truncate(2)) diff --git a/test/ruby/test_frozen.rb b/test/ruby/test_frozen.rb index 2918a2afd8..6721cb1128 100644 --- a/test/ruby/test_frozen.rb +++ b/test/ruby/test_frozen.rb @@ -27,4 +27,20 @@ class TestFrozen < Test::Unit::TestCase str.freeze assert_raise(FrozenError) { str.instance_variable_set(:@b, 1) } end + + def test_setting_ivar_on_frozen_string_with_singleton_class + str = "str" + str.singleton_class + str.freeze + assert_raise_with_message(FrozenError, "can't modify frozen String: \"str\"") { str.instance_variable_set(:@a, 1) } + end + + class A + freeze + end + + def test_setting_ivar_on_frozen_class + assert_raise_with_message(FrozenError, "can't modify frozen Class: TestFrozen::A") { A.instance_variable_set(:@a, 1) } + assert_raise_with_message(FrozenError, "can't modify frozen Class: #<Class:TestFrozen::A>") { A.singleton_class.instance_variable_set(:@a, 1) } + end end diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 7aba333e92..21448294c2 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -230,7 +230,10 @@ class TestGc < Test::Unit::TestCase GC.stat(stat) end - assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), stat_heap[:slot_size] + assert_equal GC.stat_heap(i, :slot_size), stat_heap[:slot_size] + assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] + assert_operator stat_heap[:heap_free_slots], :<=, stat[:heap_free_slots] + assert_operator stat_heap[:heap_final_slots], :<=, stat[:heap_final_slots] assert_operator stat_heap[:heap_eden_pages], :<=, stat[:heap_eden_pages] assert_operator stat_heap[:heap_eden_slots], :>=, 0 assert_operator stat_heap[:total_allocated_pages], :>=, 0 @@ -261,7 +264,7 @@ class TestGc < Test::Unit::TestCase GC.stat_heap(i, stat_heap) # Remove keys that can vary between invocations - %i(total_allocated_objects).each do |sym| + %i(total_allocated_objects heap_live_slots heap_free_slots).each do |sym| stat_heap[sym] = stat_heap_all[i][sym] = 0 end @@ -286,6 +289,9 @@ class TestGc < Test::Unit::TestCase hash.each { |k, v| stat_heap_sum[k] += v } end + assert_equal stat[:heap_live_slots], stat_heap_sum[:heap_live_slots] + assert_equal stat[:heap_free_slots], stat_heap_sum[:heap_free_slots] + assert_equal stat[:heap_final_slots], stat_heap_sum[:heap_final_slots] assert_equal stat[:heap_eden_pages], stat_heap_sum[:heap_eden_pages] assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects] @@ -312,9 +318,9 @@ class TestGc < Test::Unit::TestCase def test_latest_gc_info omit 'stress' if GC.stress - assert_separately([], __FILE__, __LINE__, <<-'RUBY') + assert_separately([{"RUBY_GC_HEAP_INIT_BYTES" => "409600"}, "-W0"], __FILE__, __LINE__, <<-'RUBY') GC.start - count = GC.stat(:heap_free_slots) + GC.stat(:heap_allocatable_slots) + count = GC.stat(:heap_free_slots) + GC.stat_heap(0, :heap_allocatable_slots) count.times{ "a" + "b" } assert_equal :newobj, GC.latest_gc_info[:gc_by] RUBY @@ -376,51 +382,36 @@ class TestGc < Test::Unit::TestCase def test_latest_gc_info_weak_references_count assert_separately([], __FILE__, __LINE__, <<~RUBY) GC.disable - count = 10_000 + COUNT = 10_000 # Some weak references may be created, so allow some margin of error error_tolerance = 100 - # Run full GC to clear out weak references - GC.start - # Run full GC again to collect stats about weak references + # Run full GC to collect stats about weak references GC.start before_weak_references_count = GC.latest_gc_info(:weak_references_count) - before_retained_weak_references_count = GC.latest_gc_info(:retained_weak_references_count) - # Create some objects and place it in a WeakMap - wmap = ObjectSpace::WeakMap.new - ary = Array.new(count) do |i| - obj = Object.new - wmap[obj] = nil - obj + # Create some WeakMaps + ary = Array.new(COUNT) + COUNT.times.with_index do |i| + ary[i] = ObjectSpace::WeakMap.new end # Run full GC to collect stats about weak references GC.start - assert_operator(GC.latest_gc_info(:weak_references_count), :>=, before_weak_references_count + count - error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :>=, before_retained_weak_references_count + count - error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, GC.latest_gc_info(:weak_references_count)) + assert_operator(GC.latest_gc_info(:weak_references_count), :>=, before_weak_references_count + COUNT - error_tolerance) before_weak_references_count = GC.latest_gc_info(:weak_references_count) - before_retained_weak_references_count = GC.latest_gc_info(:retained_weak_references_count) # Clear ary, so if ary itself is somewhere on the stack, it won't hold all references ary.clear ary = nil - # Free ary, which should empty out the wmap + # Free ary, which should GC all the WeakMaps GC.start - # Run full GC again to collect stats about weak references - GC.start - - # Sometimes the WeakMap has a few elements, which might be held on by registers. - assert_operator(wmap.size, :<=, count / 1000) - assert_operator(GC.latest_gc_info(:weak_references_count), :<=, before_weak_references_count - count + error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, before_retained_weak_references_count - count + error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, GC.latest_gc_info(:weak_references_count)) + assert_operator(GC.latest_gc_info(:weak_references_count), :<=, before_weak_references_count - COUNT + error_tolerance) RUBY end @@ -462,32 +453,19 @@ class TestGc < Test::Unit::TestCase end def test_gc_parameter - env = { - "RUBY_GC_HEAP_INIT_SLOTS" => "100" - } - assert_in_out_err([env, "-W0", "-e", "exit"], "", [], []) - assert_in_out_err([env, "-W:deprecated", "-e", "exit"], "", [], - /The environment variable RUBY_GC_HEAP_INIT_SLOTS is deprecated; use environment variables RUBY_GC_HEAP_%d_INIT_SLOTS instead/) - - env = {} - GC.stat_heap.keys.each do |heap| - env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "200000" - end + env = { "RUBY_GC_HEAP_INIT_BYTES" => "#{200000 * 40}" } assert_normal_exit("exit", "", :child_env => env) - env = {} - GC.stat_heap.keys.each do |heap| - env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "0" - end + env = { "RUBY_GC_HEAP_INIT_BYTES" => "0" } assert_normal_exit("exit", "", :child_env => env) env = { "RUBY_GC_HEAP_GROWTH_FACTOR" => "2.0", - "RUBY_GC_HEAP_GROWTH_MAX_SLOTS" => "10000" + "RUBY_GC_HEAP_GROWTH_MAX_BYTES" => "409600" } assert_normal_exit("exit", "", :child_env => env) assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_FACTOR=2.0/, "") - assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_MAX_SLOTS=10000/, "[ruby-core:57928]") + assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_MAX_BYTES=409600/, "[ruby-core:57928]") if use_rgengc? env = { @@ -529,18 +507,19 @@ class TestGc < Test::Unit::TestCase end end - def test_gc_parameter_init_slots + def test_gc_parameter_init_bytes omit "[Bug #21203] This test is flaky and intermittently failing now" assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60) - # Constant from gc.c. - GC_HEAP_INIT_SLOTS = 10_000 + GC_HEAP_INIT_BYTES = 2560 * 1024 gc_count = GC.stat(:count) - # Fill up all of the size pools to the init slots + # Fill up all heaps to the byte-derived init slot count GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| - capa = (GC.stat_heap(i, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] - while GC.stat_heap(i, :heap_eden_slots) < GC_HEAP_INIT_SLOTS + slot_size = GC.stat_heap(i, :slot_size) + init_slots = GC_HEAP_INIT_BYTES / slot_size + capa = (slot_size - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] + while GC.stat_heap(i, :heap_eden_slots) < init_slots Array.new(capa) end end @@ -548,19 +527,17 @@ class TestGc < Test::Unit::TestCase assert_equal gc_count, GC.stat(:count) RUBY - env = {} - sizes = GC.stat_heap.keys.reverse.map { 20_000 } - GC.stat_heap.keys.each do |heap| - env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = sizes[heap].to_s - end + env = { "RUBY_GC_HEAP_INIT_BYTES" => "#{800 * 1024}" } assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY, timeout: 60) - SIZES = #{sizes} + GC_HEAP_INIT_BYTES = 800 * 1024 gc_count = GC.stat(:count) - # Fill up all of the size pools to the init slots + # Fill up all heaps to the byte-derived init slot count GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| - capa = (GC.stat_heap(i, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] - while GC.stat_heap(i, :heap_eden_slots) < SIZES[i] + slot_size = GC.stat_heap(i, :slot_size) + init_slots = GC_HEAP_INIT_BYTES / slot_size + capa = (slot_size - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] + while GC.stat_heap(i, :heap_eden_slots) < init_slots Array.new(capa) end end @@ -675,10 +652,8 @@ class TestGc < Test::Unit::TestCase debug_msg = "before_stats: #{before_stats}\nbefore_stat_heap: #{before_stat_heap}\nafter_stats: #{after_stats}\nafter_stat_heap: #{after_stat_heap}" # Should not be thrashing in page creation - assert_equal before_stats[:heap_allocated_pages], after_stats[:heap_allocated_pages], debug_msg + assert_in_epsilon before_stats[:heap_allocated_pages], after_stats[:heap_allocated_pages], 0.5, debug_msg assert_equal 0, after_stats[:total_freed_pages], debug_msg - # Only young objects, so should not trigger major GC - assert_equal before_stats[:major_gc_count], after_stats[:major_gc_count], debug_msg RUBY end @@ -710,8 +685,7 @@ class TestGc < Test::Unit::TestCase end def test_gc_internals - assert_not_nil GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] - assert_not_nil GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + assert_not_nil GC::INTERNAL_CONSTANTS[:HEAP_COUNT] end def test_sweep_in_finalizer @@ -742,6 +716,7 @@ class TestGc < Test::Unit::TestCase end def test_interrupt_in_finalizer + omit 'randomly hangs on many platforms' if ENV.key?('GITHUB_ACTIONS') bug10595 = '[ruby-core:66825] [Bug #10595]' src = <<-'end;' Signal.trap(:INT, 'DEFAULT') @@ -798,7 +773,7 @@ class TestGc < Test::Unit::TestCase end def test_gc_stress_at_startup - assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 60) + assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 120) end def test_gc_disabled_start @@ -824,6 +799,8 @@ class TestGc < Test::Unit::TestCase end def test_exception_in_finalizer_procs + require '-test-/stack' + omit 'failing with ASAN' if Thread.asan? assert_in_out_err(["-W0"], "#{<<~"begin;"}\n#{<<~'end;'}", %w[c1 c2]) c1 = proc do puts "c1" @@ -844,6 +821,8 @@ class TestGc < Test::Unit::TestCase end def test_exception_in_finalizer_method + require '-test-/stack' + omit 'failing with ASAN' if Thread.asan? assert_in_out_err(["-W0"], "#{<<~"begin;"}\n#{<<~'end;'}", %w[c1 c2]) def self.c1(x) puts "c1" @@ -909,4 +888,25 @@ class TestGc < Test::Unit::TestCase assert_include ObjectSpace.dump(young_obj), '"old":true' end end + + def test_finalizer_not_run_with_vm_lock + assert_ractor(<<~'RUBY', timeout: 30) + Thread.new do + loop do + Encoding.list.each do |enc| + enc.names + end + end + end + + o = Object.new + ObjectSpace.define_finalizer(o, proc do + sleep 0.5 # finalizer shouldn't be run with VM lock, otherwise this context switch will crash + end) + o = nil + 4.times do + GC.start + end + RUBY + end end diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb index 3eaa93dfae..cb5e9d6ccb 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -30,7 +30,7 @@ class TestGCCompact < Test::Unit::TestCase def test_enable_autocompact before = GC.auto_compact GC.auto_compact = true - assert GC.auto_compact + assert_predicate GC, :auto_compact ensure GC.auto_compact = before end @@ -151,12 +151,12 @@ class TestGCCompact < Test::Unit::TestCase def walk_ast ast children = ast.children.grep(RubyVM::AbstractSyntaxTree::Node) children.each do |child| - assert child.type + assert_predicate child, :type walk_ast child end end ast = RubyVM::AbstractSyntaxTree.parse_file #{__FILE__.dump} - assert GC.compact + assert_predicate GC, :compact walk_ast ast end; end @@ -207,7 +207,7 @@ class TestGCCompact < Test::Unit::TestCase end def test_updating_references_for_embed_shared_arrays - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -256,7 +256,7 @@ class TestGCCompact < Test::Unit::TestCase end def test_updating_references_for_embed_frozen_shared_arrays - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -284,7 +284,7 @@ class TestGCCompact < Test::Unit::TestCase end def test_moving_arrays_down_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -300,13 +300,13 @@ class TestGCCompact < Test::Unit::TestCase }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats.dig(:moved_down, :T_ARRAY) || 0, :>=, ARY_COUNT - 10) + assert_operator(stats.dig(:moved_down, :T_ARRAY) || 0, :>=, ARY_COUNT - 25) refute_empty($arys.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end def test_moving_arrays_up_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -315,7 +315,7 @@ class TestGCCompact < Test::Unit::TestCase GC.verify_compaction_references(expand_heap: true, toward: :empty) Fiber.new { - ary = "hello".chars + ary = "hello world".chars # > 6 elements to exceed pool 0 embed capacity $arys = ARY_COUNT.times.map do x = [] ary.each { |e| x << e } @@ -324,13 +324,13 @@ class TestGCCompact < Test::Unit::TestCase }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats.dig(:moved_up, :T_ARRAY) || 0, :>=, ARY_COUNT - 10) + assert_operator(stats.dig(:moved_up, :T_ARRAY) || 0, :>=, (0.9995 * ARY_COUNT).to_i) refute_empty($arys.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end def test_moving_objects_between_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 60) begin; @@ -356,13 +356,29 @@ class TestGCCompact < Test::Unit::TestCase stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats.dig(:moved_up, :T_OBJECT) || 0, :>=, OBJ_COUNT - 10) + assert_operator(stats.dig(:moved_up, :T_OBJECT) || 0, :>=, OBJ_COUNT - 25) refute_empty($ary.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end + def test_compact_objects_of_varying_sizes + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 + + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) + begin; + $objects = [] + 160.times do |n| + obj = Class.new.new + n.times { |i| obj.instance_variable_set("@foo" + i.to_s, 0) } + $objects << obj + end + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + end; + end + def test_moving_strings_up_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 30) begin; @@ -371,19 +387,19 @@ class TestGCCompact < Test::Unit::TestCase GC.verify_compaction_references(expand_heap: true, toward: :empty) Fiber.new { - str = "a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 4 + str = "a" * GC.stat_heap(0, :slot_size) * 4 $ary = STR_COUNT.times.map { +"" << str } }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats[:moved_up][:T_STRING], :>=, STR_COUNT - 10) + assert_operator(stats[:moved_up][:T_STRING], :>=, STR_COUNT - 25) refute_empty($ary.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end def test_moving_strings_down_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 30) begin; @@ -392,18 +408,18 @@ class TestGCCompact < Test::Unit::TestCase GC.verify_compaction_references(expand_heap: true, toward: :empty) Fiber.new { - $ary = STR_COUNT.times.map { ("a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 4).squeeze! } + $ary = STR_COUNT.times.map { ("a" * GC.stat_heap(0, :slot_size) * 4).squeeze! } }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats[:moved_down][:T_STRING], :>=, STR_COUNT - 10) + assert_operator(stats[:moved_down][:T_STRING], :>=, STR_COUNT - 25) refute_empty($ary.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end def test_moving_hashes_down_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 # AR and ST hashes are in the same size pool on 32 bit omit unless RbConfig::SIZEOF["uint64_t"] <= RbConfig::SIZEOF["void*"] @@ -421,7 +437,7 @@ class TestGCCompact < Test::Unit::TestCase stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats[:moved_down][:T_HASH], :>=, HASH_COUNT - 10) + assert_operator(stats[:moved_down][:T_HASH], :>=, HASH_COUNT - 25) end; end @@ -453,7 +469,7 @@ class TestGCCompact < Test::Unit::TestCase end; end - def test_moving_too_complex_generic_ivar + def test_moving_complex_generic_ivar omit "not compiled with SHAPE_DEBUG" unless defined?(RubyVM::Shape) assert_separately([], <<~RUBY) diff --git a/test/ruby/test_hash.rb b/test/ruby/test_hash.rb index 576a5f6064..2d1b513c70 100644 --- a/test/ruby/test_hash.rb +++ b/test/ruby/test_hash.rb @@ -465,10 +465,10 @@ class TestHash < Test::Unit::TestCase def test_each_value res = [] @cls[].each_value { |v| res << v } - assert_equal(0, [].length) + assert_equal(0, res.length) @h.each_value { |v| res << v } - assert_equal(0, [].length) + assert_equal(@h.size, res.length) expected = [] @h.each { |k, v| expected << v } @@ -2007,7 +2007,7 @@ class TestHashOnly < Test::Unit::TestCase EnvUtil.without_gc do before = ObjectSpace.count_objects[:T_STRING] - 5.times{ h["abc"] } + 5.times{ h["abc".freeze] } assert_equal before, ObjectSpace.count_objects[:T_STRING] end end diff --git a/test/ruby/test_integer.rb b/test/ruby/test_integer.rb index f9bf4fa20c..c3d9d311c8 100644 --- a/test/ruby/test_integer.rb +++ b/test/ruby/test_integer.rb @@ -751,7 +751,7 @@ class TestInteger < Test::Unit::TestCase o = Object.new def o.to_int; Object.new; end - assert_raise_with_message(TypeError, /can't convert Object to Integer/) {Integer.try_convert(o)} + assert_raise_with_message(TypeError, /can't convert Object into Integer/) {Integer.try_convert(o)} end def test_ceildiv diff --git a/test/ruby/test_io.rb b/test/ruby/test_io.rb index 3ec8726b5e..a78527d40e 100644 --- a/test/ruby/test_io.rb +++ b/test/ruby/test_io.rb @@ -467,6 +467,24 @@ class TestIO < Test::Unit::TestCase } end + def test_each_codepoint_with_ungetc + bug21562 = '[ruby-core:123176] [Bug #21562]' + with_read_pipe("") {|p| + p.binmode + p.ungetc("aa") + a = "" + p.each_codepoint { |c| a << c } + assert_equal("aa", a, bug21562) + } + with_read_pipe("") {|p| + p.set_encoding("ascii-8bit", universal_newline: true) + p.ungetc("aa") + a = "" + p.each_codepoint { |c| a << c } + assert_equal("aa", a, bug21562) + } + end + def test_rubydev33072 t = make_tempfile path = t.path @@ -1375,10 +1393,6 @@ class TestIO < Test::Unit::TestCase args = ['-e', '$>.write($<.read)'] if args.empty? ruby = EnvUtil.rubybin opts = {} - if defined?(Process::RLIMIT_NPROC) - lim = Process.getrlimit(Process::RLIMIT_NPROC)[1] - opts[:rlimit_nproc] = [lim, 2048].min - end f = IO.popen([ruby] + args, 'r+', opts) pid = f.pid yield(f) @@ -2601,36 +2615,15 @@ class TestIO < Test::Unit::TestCase assert_equal({:a=>1}, open(o, {a: 1})) end - def test_open_pipe - assert_deprecated_warning(/Kernel#open with a leading '\|'/) do # https://bugs.ruby-lang.org/issues/19630 - open("|" + EnvUtil.rubybin, "r+") do |f| - f.puts "puts 'foo'" - f.close_write - assert_equal("foo\n", f.read) - end - end - end + def test_path_with_pipe + mkcdtmpdir do + cmd = "|echo foo" + assert_file.not_exist?(cmd) - def test_read_command - assert_deprecated_warning(/IO process creation with a leading '\|'/) do # https://bugs.ruby-lang.org/issues/19630 - assert_equal("foo\n", IO.read("|echo foo")) - end - assert_raise(Errno::ENOENT, Errno::EINVAL) do - File.read("|#{EnvUtil.rubybin} -e puts") - end - assert_raise(Errno::ENOENT, Errno::EINVAL) do - File.binread("|#{EnvUtil.rubybin} -e puts") - end - assert_raise(Errno::ENOENT, Errno::EINVAL) do - Class.new(IO).read("|#{EnvUtil.rubybin} -e puts") - end - assert_raise(Errno::ENOENT, Errno::EINVAL) do - Class.new(IO).binread("|#{EnvUtil.rubybin} -e puts") - end - assert_raise(Errno::ESPIPE) do - assert_deprecated_warning(/IO process creation with a leading '\|'/) do # https://bugs.ruby-lang.org/issues/19630 - IO.read("|#{EnvUtil.rubybin} -e 'puts :foo'", 1, 1) - end + pipe_errors = [Errno::ENOENT, Errno::EINVAL, Errno::EACCES, Errno::EPERM] + assert_raise(*pipe_errors) { open(cmd, "r+") } + assert_raise(*pipe_errors) { IO.read(cmd) } + assert_raise(*pipe_errors) { IO.foreach(cmd) {|x| assert false } } end end @@ -2835,19 +2828,6 @@ class TestIO < Test::Unit::TestCase end def test_foreach - a = [] - - assert_deprecated_warning(/IO process creation with a leading '\|'/) do # https://bugs.ruby-lang.org/issues/19630 - IO.foreach("|" + EnvUtil.rubybin + " -e 'puts :foo; puts :bar; puts :baz'") {|x| a << x } - end - assert_equal(["foo\n", "bar\n", "baz\n"], a) - - a = [] - assert_deprecated_warning(/IO process creation with a leading '\|'/) do # https://bugs.ruby-lang.org/issues/19630 - IO.foreach("|" + EnvUtil.rubybin + " -e 'puts :zot'", :open_args => ["r"]) {|x| a << x } - end - assert_equal(["zot\n"], a) - make_tempfile {|t| a = [] IO.foreach(t.path) {|x| a << x } @@ -2923,10 +2903,10 @@ class TestIO < Test::Unit::TestCase end def test_print_separators - EnvUtil.suppress_warning { - $, = ':' - $\ = "\n" - } + assert_deprecated_warning(/non-nil '\$,'/) {$, = ":"} + assert_raise(TypeError) {$, = 1} + assert_deprecated_warning(/non-nil '\$\\'/) {$\ = "\n"} + assert_raise(TypeError) {$/ = 1} pipe(proc do |w| w.print('a') EnvUtil.suppress_warning {w.print('a','b','c')} diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 62c4667888..b6372f25b8 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -1,6 +1,7 @@ # frozen_string_literal: false require 'tempfile' +require 'rbconfig/sizeof' class TestIOBuffer < Test::Unit::TestCase experimental = Warning[:experimental] @@ -45,22 +46,22 @@ class TestIOBuffer < Test::Unit::TestCase def test_new_internal buffer = IO::Buffer.new(1024, IO::Buffer::INTERNAL) assert_equal 1024, buffer.size - refute buffer.external? - assert buffer.internal? - refute buffer.mapped? + refute_predicate buffer, :external? + assert_predicate buffer, :internal? + refute_predicate buffer, :mapped? end def test_new_mapped buffer = IO::Buffer.new(1024, IO::Buffer::MAPPED) assert_equal 1024, buffer.size - refute buffer.external? - refute buffer.internal? - assert buffer.mapped? + refute_predicate buffer, :external? + refute_predicate buffer, :internal? + assert_predicate buffer, :mapped? end def test_new_readonly buffer = IO::Buffer.new(128, IO::Buffer::INTERNAL|IO::Buffer::READONLY) - assert buffer.readonly? + assert_predicate buffer, :readonly? assert_raise IO::Buffer::AccessError do buffer.set_string("") @@ -73,12 +74,64 @@ class TestIOBuffer < Test::Unit::TestCase def test_file_mapped buffer = File.open(__FILE__) {|file| IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)} - contents = buffer.get_string + assert_equal File.size(__FILE__), buffer.size + contents = buffer.get_string assert_include contents, "Hello World" assert_equal Encoding::BINARY, contents.encoding end + def test_file_mapped_with_size + buffer = File.open(__FILE__) {|file| IO::Buffer.map(file, 30, 0, IO::Buffer::READONLY)} + assert_equal 30, buffer.size + + contents = buffer.get_string + assert_equal "# frozen_string_literal: false", contents + assert_equal Encoding::BINARY, contents.encoding + end + + def test_file_mapped_size_too_large + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 200_000, 0, IO::Buffer::READONLY)} + end + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, File.size(__FILE__) + 1, 0, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_size_just_enough + File.open(__FILE__) {|file| + assert_equal File.size(__FILE__), IO::Buffer.map(file, File.size(__FILE__), 0, IO::Buffer::READONLY).size + } + end + + def test_file_mapped_offset_too_large + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, nil, IO::Buffer::PAGE_SIZE * 100, IO::Buffer::READONLY)} + end + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 20, IO::Buffer::PAGE_SIZE * 100, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_zero_size + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 0, 0, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_negative_size + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, -10, 0, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_negative_offset + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 20, -1, IO::Buffer::READONLY)} + end + end + def test_file_mapped_invalid assert_raise TypeError do IO::Buffer.map("foobar") @@ -88,19 +141,19 @@ class TestIOBuffer < Test::Unit::TestCase def test_string_mapped string = "Hello World" buffer = IO::Buffer.for(string) - assert buffer.readonly? + assert_predicate buffer, :readonly? end def test_string_mapped_frozen string = "Hello World".freeze buffer = IO::Buffer.for(string) - assert buffer.readonly? + assert_predicate buffer, :readonly? end def test_string_mapped_mutable string = "Hello World" IO::Buffer.for(string) do |buffer| - refute buffer.readonly? + refute_predicate buffer, :readonly? buffer.set_value(:U8, 0, "h".ord) @@ -353,10 +406,17 @@ class TestIOBuffer < Test::Unit::TestCase :u64 => [0, 2**64-1], :s64 => [-2**63, 0, 2**63-1], + :U128 => [0, 2**64, 2**127-1, 2**128-1], + :S128 => [-2**127, -2**63-1, -1, 0, 2**63, 2**127-1], + :u128 => [0, 2**64, 2**127-1, 2**128-1], + :s128 => [-2**127, -2**63-1, -1, 0, 2**63, 2**127-1], + :F32 => [-1.0, 0.0, 0.5, 1.0, 128.0], :F64 => [-1.0, 0.0, 0.5, 1.0, 128.0], } + SIZE_MAX = RbConfig::LIMITS["SIZE_MAX"] + def test_get_set_value buffer = IO::Buffer.new(128) @@ -365,6 +425,16 @@ class TestIOBuffer < Test::Unit::TestCase buffer.set_value(data_type, 0, value) assert_equal value, buffer.get_value(data_type, 0), "Converting #{value} as #{data_type}." end + assert_raise(ArgumentError) {buffer.get_value(data_type, 128)} + assert_raise(ArgumentError) {buffer.set_value(data_type, 128, 0)} + case data_type + when :U8, :S8 + else + assert_raise(ArgumentError) {buffer.get_value(data_type, 127)} + assert_raise(ArgumentError) {buffer.set_value(data_type, 127, 0)} + assert_raise(ArgumentError) {buffer.get_value(data_type, SIZE_MAX)} + assert_raise(ArgumentError) {buffer.set_value(data_type, SIZE_MAX, 0)} + end end end @@ -421,6 +491,15 @@ class TestIOBuffer < Test::Unit::TestCase buffer = IO::Buffer.for(string) assert_equal string.bytes, buffer.each_byte.to_a + assert_equal string.bytes[3, 5], buffer.each_byte(3, 5).to_a + end + + def test_each_byte_bounds_error + buffer = IO::Buffer.for("A") + + assert_raise(ArgumentError) { buffer.each_byte(0, 2).to_a } + assert_raise(ArgumentError) { buffer.each_byte(1, 1).to_a } + assert_raise(ArgumentError) { buffer.each_byte(SIZE_MAX, 0).to_a } end def test_zero_length_each_byte @@ -431,7 +510,21 @@ class TestIOBuffer < Test::Unit::TestCase def test_clear buffer = IO::Buffer.new(16) - buffer.set_string("Hello World!") + assert_equal "\0" * 16, buffer.get_string + buffer.clear(1) + assert_equal "\1" * 16, buffer.get_string + buffer.clear(2, 1, 2) + assert_equal "\1" + "\2"*2 + "\1"*13, buffer.get_string + buffer.clear(2, 1) + assert_equal "\1" + "\2"*15, buffer.get_string + buffer.clear(260) + assert_equal "\4" * 16, buffer.get_string + assert_raise(TypeError) {buffer.clear("x")} + + assert_raise(ArgumentError) {buffer.clear(0, 20)} + assert_raise(ArgumentError) {buffer.clear(0, 0, 20)} + assert_raise(ArgumentError) {buffer.clear(0, 10, 10)} + assert_raise(ArgumentError) {buffer.clear(0, SIZE_MAX-7, 10)} end def test_invalidation @@ -609,6 +702,59 @@ class TestIOBuffer < Test::Unit::TestCase assert_equal IO::Buffer.for("\xce\xcd\xcc\xcb\xce\xcd\xcc\xcb\xce\xcd"), source.dup.not! end + def test_operators_raise_on_freed_self + inner = IO::Buffer.new(IO::Buffer::PAGE_SIZE) + slice = inner.slice(0, 8) + inner.free + + mask = IO::Buffer.for("ABCDEFGH") + assert_raise(IO::Buffer::InvalidatedError) { slice & mask } + assert_raise(IO::Buffer::InvalidatedError) { slice | mask } + assert_raise(IO::Buffer::InvalidatedError) { slice ^ mask } + assert_raise(IO::Buffer::InvalidatedError) { ~slice } + end + + def test_operators_raise_on_freed_mask + inner = IO::Buffer.new(IO::Buffer::PAGE_SIZE) + mask_slice = inner.slice(0, 8) + inner.free + + source = IO::Buffer.for("ABCDEFGH") + assert_raise(IO::Buffer::InvalidatedError) { source & mask_slice } + assert_raise(IO::Buffer::InvalidatedError) { source | mask_slice } + assert_raise(IO::Buffer::InvalidatedError) { source ^ mask_slice } + end + + def test_bit_count + # All ones: 8 bits set per byte + assert_equal 8, IO::Buffer.for("\xFF").bit_count + # All zeros: no bits set + assert_equal 0, IO::Buffer.for("\x00").bit_count + # Mixed: 0xFF (8) + 0x00 (0) + 0x0F (4) = 12 + assert_equal 12, IO::Buffer.for("\xFF\x00\x0F").bit_count + # Subrange: offset=0, length=1 => 0xFF => 8 + assert_equal 8, IO::Buffer.for("\xFF\x00\x0F").bit_count(0, 1) + # Subrange: offset=1, length=1 => 0x00 => 0 + assert_equal 0, IO::Buffer.for("\xFF\x00\x0F").bit_count(1, 1) + # Subrange: offset=2, length=1 => 0x0F => 4 + assert_equal 4, IO::Buffer.for("\xFF\x00\x0F").bit_count(2, 1) + # Subrange: offset=1, length=2 => 0x00 + 0x0F = 4 + assert_equal 4, IO::Buffer.for("\xFF\x00\x0F").bit_count(1, 2) + # Empty buffer: 0 + assert_equal 0, IO::Buffer.new(0).bit_count + # 8-byte aligned: 8 bytes of 0xFF => 64 bits + assert_equal 64, IO::Buffer.for("\xFF" * 8).bit_count + # Cross 8-byte boundary: 9 bytes of 0xFF => 72 bits + assert_equal 72, IO::Buffer.for("\xFF" * 9).bit_count + # offset=0 with no length => defaults to full buffer: + assert_equal 12, IO::Buffer.for("\xFF\x00\x0F").bit_count(0) + # offset=1 with no length => 0x00 + 0x0F = 4: + assert_equal 4, IO::Buffer.for("\xFF\x00\x0F").bit_count(1) + # Out-of-range raises + assert_raise(ArgumentError) { IO::Buffer.for("\xFF").bit_count(0, 2) } + assert_raise(ArgumentError) { IO::Buffer.for("\xFF").bit_count(1, 1) } + end + def test_shared message = "Hello World" buffer = IO::Buffer.new(64, IO::Buffer::MAPPED | IO::Buffer::SHARED) @@ -630,8 +776,8 @@ class TestIOBuffer < Test::Unit::TestCase buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) begin - assert buffer.private? - refute buffer.readonly? + assert_predicate buffer, :private? + refute_predicate buffer, :readonly? buffer.set_string("J") @@ -706,4 +852,217 @@ class TestIOBuffer < Test::Unit::TestCase assert_predicate buf, :valid? end + + def test_128_bit_integers + buffer = IO::Buffer.new(32) + + # Test unsigned 128-bit integers + test_values_u128 = [ + 0, + 1, + 2**64 - 1, + 2**64, + 2**127 - 1, + 2**128 - 1, + ] + + test_values_u128.each do |value| + buffer.set_value(:u128, 0, value) + assert_equal value, buffer.get_value(:u128, 0), "u128: #{value}" + + buffer.set_value(:U128, 0, value) + assert_equal value, buffer.get_value(:U128, 0), "U128: #{value}" + end + + # Test signed 128-bit integers + test_values_s128 = [ + -2**127, + -2**63 - 1, + -1, + 0, + 1, + 2**63, + 2**127 - 1, + ] + + test_values_s128.each do |value| + buffer.set_value(:s128, 0, value) + assert_equal value, buffer.get_value(:s128, 0), "s128: #{value}" + + buffer.set_value(:S128, 0, value) + assert_equal value, buffer.get_value(:S128, 0), "S128: #{value}" + end + + # Test size_of + assert_equal 16, IO::Buffer.size_of(:u128) + assert_equal 16, IO::Buffer.size_of(:U128) + assert_equal 16, IO::Buffer.size_of(:s128) + assert_equal 16, IO::Buffer.size_of(:S128) + assert_equal 32, IO::Buffer.size_of([:u128, :u128]) + end + + def test_integer_endianness_swapping + # Test that byte order is swapped correctly for all signed and unsigned integers > 1 byte + host_is_le = IO::Buffer::HOST_ENDIAN == IO::Buffer::LITTLE_ENDIAN + host_is_be = IO::Buffer::HOST_ENDIAN == IO::Buffer::BIG_ENDIAN + + # Test values that will produce different byte patterns when swapped + # Format: [little_endian_type, big_endian_type, test_value, expected_swapped_value] + # expected_swapped_value is the result when writing as le_type and reading as be_type + # (or vice versa) on a little-endian host + test_cases = [ + [:u16, :U16, 0x1234, 0x3412], + [:s16, :S16, 0x1234, 0x3412], + [:u32, :U32, 0x12345678, 0x78563412], + [:s32, :S32, 0x12345678, 0x78563412], + [:u64, :U64, 0x0123456789ABCDEF, 0xEFCDAB8967452301], + [:s64, :S64, 0x0123456789ABCDEF, -1167088121787636991], + [:u128, :U128, 0x0123456789ABCDEF0123456789ABCDEF, 0xEFCDAB8967452301EFCDAB8967452301], + [:u128, :U128, 0x0123456789ABCDEFFEDCBA9876543210, 0x1032547698BADCFEEFCDAB8967452301], + [:u128, :U128, 0xFEDCBA98765432100123456789ABCDEF, 0xEFCDAB89674523011032547698BADCFE], + [:u128, :U128, 0x123456789ABCDEF0FEDCBA9876543210, 0x1032547698BADCFEF0DEBC9A78563412], + [:s128, :S128, 0x0123456789ABCDEF0123456789ABCDEF, -21528975894082904073953971026863512831], + [:s128, :S128, 0x0123456789ABCDEFFEDCBA9876543210, 0x1032547698BADCFEEFCDAB8967452301], + ] + + test_cases.each do |le_type, be_type, value, expected_swapped| + buffer_size = IO::Buffer.size_of(le_type) + buffer = IO::Buffer.new(buffer_size * 2) + + # Test little-endian round-trip + buffer.set_value(le_type, 0, value) + result_le = buffer.get_value(le_type, 0) + assert_equal value, result_le, "#{le_type}: round-trip failed" + + # Test big-endian round-trip + buffer.set_value(be_type, buffer_size, value) + result_be = buffer.get_value(be_type, buffer_size) + assert_equal value, result_be, "#{be_type}: round-trip failed" + + # Verify byte patterns are different when endianness differs from host + if host_is_le + # On little-endian host: le_type should match host, be_type should be swapped + # So the byte patterns should be different (unless value is symmetric) + # Read back with opposite endianness to verify swapping + result_le_read_as_be = buffer.get_value(be_type, 0) + result_be_read_as_le = buffer.get_value(le_type, buffer_size) + + # The swapped reads should NOT equal the original value (unless it's symmetric) + # For most values, this will be different + if value != 0 && value != -1 && value.abs != 1 + refute_equal value, result_le_read_as_be, "#{le_type} written, read as #{be_type} should be swapped on LE host" + refute_equal value, result_be_read_as_le, "#{be_type} written, read as #{le_type} should be swapped on LE host" + end + + # Verify that reading back with correct endianness works + assert_equal value, buffer.get_value(le_type, 0), "#{le_type} should read correctly on LE host" + assert_equal value, buffer.get_value(be_type, buffer_size), "#{be_type} should read correctly on LE host (with swapping)" + elsif host_is_be + # On big-endian host: be_type should match host, le_type should be swapped + result_le_read_as_be = buffer.get_value(be_type, 0) + result_be_read_as_le = buffer.get_value(le_type, buffer_size) + + # The swapped reads should NOT equal the original value (unless it's symmetric) + if value != 0 && value != -1 && value.abs != 1 + refute_equal value, result_le_read_as_be, "#{le_type} written, read as #{be_type} should be swapped on BE host" + refute_equal value, result_be_read_as_le, "#{be_type} written, read as #{le_type} should be swapped on BE host" + end + + # Verify that reading back with correct endianness works + assert_equal value, buffer.get_value(be_type, buffer_size), "#{be_type} should read correctly on BE host" + assert_equal value, buffer.get_value(le_type, 0), "#{le_type} should read correctly on BE host (with swapping)" + end + + # Verify that when we write with one endianness and read with the opposite, + # we get the expected swapped value + buffer.set_value(le_type, 0, value) + swapped_value_le_to_be = buffer.get_value(be_type, 0) + assert_equal expected_swapped, swapped_value_le_to_be, "#{le_type} written, read as #{be_type} should produce expected swapped value" + + # Also verify the reverse direction + buffer.set_value(be_type, buffer_size, value) + swapped_value_be_to_le = buffer.get_value(le_type, buffer_size) + assert_equal expected_swapped, swapped_value_be_to_le, "#{be_type} written, read as #{le_type} should produce expected swapped value" + + # Verify that writing the swapped value back and reading with original endianness + # gives us the original value (double-swap should restore original) + buffer.set_value(be_type, 0, swapped_value_le_to_be) + round_trip_value = buffer.get_value(le_type, 0) + assert_equal value, round_trip_value, "#{le_type}/#{be_type}: double-swap should restore original value" + end + end + + class Bug21882 < RuntimeError; end + def test_locked_exception + buf = IO::Buffer.new(10) + assert_raise(Bug21882, '#locked should propagate exception') do + buf.locked { raise Bug21882 } + end + + # should be unlocked now and can be locked again + refute_predicate buf, :locked? + buf.locked { } + end + + def test_locked_break + buf = IO::Buffer.new(10) + assert_equal :ok, (buf.locked { break :ok }) + + # should be unlocked now and can be locked again + refute_predicate buf, :locked? + buf.locked { } + end + + def test_locked_throw + buf = IO::Buffer.new(10) + assert_equal :ok, (catch(:bug21882) { buf.locked { throw :bug21882, :ok } }) + + # should be unlocked now and can be locked again + refute_predicate buf, :locked? + buf.locked { } + end + + def test_hexdump_default_width + buffer = IO::Buffer.for("Hello World") + hexdump = buffer.hexdump + assert_include hexdump, "Hello World" + assert_include hexdump, "0x00000000" + end + + def test_hexdump_custom_width + buffer = IO::Buffer.for("A" * 64) + hexdump = buffer.hexdump(0, 64, 32) + assert_include hexdump, "0x00000000" + assert_include hexdump, "0x00000020" + end + + def test_hexdump_maximum_width + buffer = IO::Buffer.for("A" * 2048) + # Maximum width is 1024 + hexdump = buffer.hexdump(0, 1024, 1024) + assert_include hexdump, "0x00000000" + end + + def test_hexdump_width_too_large + buffer = IO::Buffer.for("A") + # Width exceeding maximum (1024) should raise ArgumentError + assert_raise(ArgumentError) do + buffer.hexdump(0, 1, 1025) + end + end + + def test_hexdump_width_negative + buffer = IO::Buffer.for("A") + assert_raise(ArgumentError) do + buffer.hexdump(0, 1, -1) + end + end + + def test_hexdump_width_zero + buffer = IO::Buffer.for("A") + # Width must be at least 1 + assert_raise(ArgumentError) do + buffer.hexdump(0, 1, 0) + end + end end diff --git a/test/ruby/test_io_m17n.rb b/test/ruby/test_io_m17n.rb index b01d627d92..83d4fb0c7b 100644 --- a/test/ruby/test_io_m17n.rb +++ b/test/ruby/test_io_m17n.rb @@ -1395,30 +1395,6 @@ EOT } end - def test_open_pipe_r_enc - EnvUtil.suppress_warning do # https://bugs.ruby-lang.org/issues/19630 - open("|#{EnvUtil.rubybin} -e 'putc 255'", "r:ascii-8bit") {|f| - assert_equal(Encoding::ASCII_8BIT, f.external_encoding) - assert_equal(nil, f.internal_encoding) - s = f.read - assert_equal(Encoding::ASCII_8BIT, s.encoding) - assert_equal("\xff".force_encoding("ascii-8bit"), s) - } - end - end - - def test_open_pipe_r_enc2 - EnvUtil.suppress_warning do # https://bugs.ruby-lang.org/issues/19630 - open("|#{EnvUtil.rubybin} -e 'putc \"\\u3042\"'", "r:UTF-8") {|f| - assert_equal(Encoding::UTF_8, f.external_encoding) - assert_equal(nil, f.internal_encoding) - s = f.read - assert_equal(Encoding::UTF_8, s.encoding) - assert_equal("\u3042", s) - } - end - end - def test_s_foreach_enc with_tmpdir { generate_file("t", "\xff") @@ -2748,8 +2724,8 @@ EOT def test_pos_with_buffer_end_cr bug6401 = '[ruby-core:44874]' with_tmpdir { - # Read buffer size is 8191. This generates '\r' at 8191. - lines = ["X" * 8187, "X"] + # Read buffer size is 8192. This generates '\r' at 8192. + lines = ["X" * 8188, "X"] generate_file("tmp", lines.join("\r\n") + "\r\n") open("tmp", "r") do |f| @@ -2830,4 +2806,17 @@ EOT flunk failure.join("\n---\n") end end + + def test_each_codepoint_encoding_with_ungetc + File.open(File::NULL, "rt:utf-8") do |f| + f.ungetc(%Q[\u{3042}\u{3044}\u{3046}]) + assert_equal [0x3042, 0x3044, 0x3046], f.each_codepoint.to_a + end + File.open(File::NULL, "rt:us-ascii") do |f| + f.ungetc(%Q[\u{3042}\u{3044}\u{3046}]) + assert_raise(ArgumentError) do + f.each_codepoint.to_a + end + end + end end diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb index 45223c89da..b4760dc412 100644 --- a/test/ruby/test_iseq.rb +++ b/test/ruby/test_iseq.rb @@ -139,8 +139,7 @@ class TestISeq < Test::Unit::TestCase def test_lambda_with_ractor_roundtrip iseq = compile(<<~EOF, __LINE__+1) x = 42 - y = nil.instance_eval{ lambda { x } } - Ractor.make_shareable(y) + y = Ractor.shareable_lambda{x} y.call EOF assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) @@ -158,22 +157,18 @@ class TestISeq < Test::Unit::TestCase def test_ractor_unshareable_outer_variable name = "\u{2603 26a1}" - y = nil.instance_eval do - eval("proc {#{name} = nil; proc {|x| #{name} = x}}").call + assert_raise_with_message(Ractor::IsolationError, /\(#{name}\)/) do + eval("#{name} = nil; Ractor.shareable_proc{#{name} = nil}") end - assert_raise_with_message(ArgumentError, /\(#{name}\)/) do - Ractor.make_shareable(y) - end - y = nil.instance_eval do - eval("proc {#{name} = []; proc {|x| #{name}}}").call - end - assert_raise_with_message(Ractor::IsolationError, /'#{name}'/) do - Ractor.make_shareable(y) + + assert_raise_with_message(Ractor::IsolationError, /\'#{name}\'/) do + eval("#{name} = []; Ractor.shareable_proc{#{name}}") end + obj = Object.new - def obj.foo(*) nil.instance_eval{ ->{super} } end - assert_raise_with_message(Ractor::IsolationError, /refer unshareable object \[\] from variable '\*'/) do - Ractor.make_shareable(obj.foo(*[])) + def obj.foo(*) Ractor.shareable_proc{super} end + assert_raise_with_message(Ractor::IsolationError, /cannot make a shareable Proc because it can refer unshareable object \[\]/) do + obj.foo(*[]) end end @@ -217,6 +212,26 @@ class TestISeq < Test::Unit::TestCase end end + def test_compile_file_options + Tempfile.create(%w"test_iseq .rb") do |f| + f.puts('_ = "test"') + f.close + iseq = RubyVM::InstructionSequence.compile_file(f.path, { frozen_string_literal: false }) + refute_predicate iseq.eval, :frozen? + + iseq = RubyVM::InstructionSequence.compile_file(f.path, { frozen_string_literal: true }) + assert_predicate iseq.eval, :frozen? + end + end + + def test_compile_options + iseq = RubyVM::InstructionSequence.compile("'test'", nil, nil, nil, { frozen_string_literal: false }) + refute_predicate iseq.eval, :frozen? + + iseq = RubyVM::InstructionSequence.compile("'test'", nil, nil, nil, { frozen_string_literal: true }) + assert_predicate iseq.eval, :frozen? + end + LINE_BEFORE_METHOD = __LINE__ def method_test_line_trace @@ -360,6 +375,20 @@ class TestISeq < Test::Unit::TestCase assert_not_predicate(s4, :frozen?) end + def test_frozen_string_literal_compile_option_file + Tempfile.create(%w[fsl .rb]) do |f| + f.write("['foo', 'foo', \"\#{$f}foo\", \"\#{'foo'}\"]\n") + f.flush + $f = 'f' + s1, s2, s3, s4 = RubyVM::InstructionSequence + .compile_file(f.path, frozen_string_literal: true).eval + assert_predicate(s1, :frozen?) + assert_predicate(s2, :frozen?) + assert_not_predicate(s3, :frozen?) + assert_not_predicate(s4, :frozen?) + end + end + # Safe call chain is not optimized when Coverage is running. # So we can test it only when Coverage is not running. def test_safe_call_chain @@ -687,6 +716,17 @@ class TestISeq < Test::Unit::TestCase assert_equal([[:nokey]], iseq.eval.singleton_method(:foo).parameters) end + def test_to_binary_dumps_noblock + iseq = assert_iseq_to_binary(<<-RUBY) + o = Object.new + class << o + def foo(&nil); end + end + o + RUBY + assert_equal([[:noblock]], iseq.eval.singleton_method(:foo).parameters) + end + def test_to_binary_line_info assert_iseq_to_binary("#{<<~"begin;"}\n#{<<~'end;'}", '[Bug #14660]').eval begin; @@ -866,6 +906,10 @@ class TestISeq < Test::Unit::TestCase assert_ruby_status([], "BEGIN {exit}; while true && true; end") end + def test_short_circuited_loop_condition + assert_ruby_status([], "while true || true; exit; end; abort") + end + def test_unreachable_syntax_error mesg = /Invalid break/ assert_syntax_error("false and break", mesg) diff --git a/test/ruby/test_lambda.rb b/test/ruby/test_lambda.rb index 3cbb54306c..ce0f338760 100644 --- a/test/ruby/test_lambda.rb +++ b/test/ruby/test_lambda.rb @@ -163,7 +163,7 @@ class TestLambdaParameters < Test::Unit::TestCase end def test_proc_inside_lambda_toplevel - assert_separately [], <<~RUBY + assert_ruby_status [], <<~RUBY lambda{ $g = proc{ return :pr } }.call @@ -276,27 +276,27 @@ class TestLambdaParameters < Test::Unit::TestCase end def test_do_lambda_source_location - exp = [__LINE__ + 1, 12, __LINE__ + 5, 7] + exp_lineno = __LINE__ + 3 lmd = ->(x, y, z) do # end - file, *loc = lmd.source_location + file, lineno = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp, loc) + assert_equal(exp_lineno, lineno, "must be at the beginning of the block") end def test_brace_lambda_source_location - exp = [__LINE__ + 1, 12, __LINE__ + 5, 5] + exp_lineno = __LINE__ + 3 lmd = ->(x, y, z) { # } - file, *loc = lmd.source_location + file, lineno = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp, loc) + assert_equal(exp_lineno, lineno, "must be at the beginning of the block") end def test_not_orphan_return diff --git a/test/ruby/test_lazy_enumerator.rb b/test/ruby/test_lazy_enumerator.rb index 4dddbab50c..3652096237 100644 --- a/test/ruby/test_lazy_enumerator.rb +++ b/test/ruby/test_lazy_enumerator.rb @@ -608,7 +608,7 @@ EOS end def test_require_block - %i[select reject drop_while take_while map flat_map].each do |method| + %i[select reject drop_while take_while map flat_map tap_each].each do |method| assert_raise(ArgumentError){ [].lazy.send(method) } end end @@ -715,4 +715,23 @@ EOS def test_with_index_size assert_equal(3, Enumerator::Lazy.new([1, 2, 3], 3){|y, v| y << v}.with_index.size) end + + def test_tap_each + out = [] + + e = (1..Float::INFINITY).lazy + .tap_each { |x| out << x } + .select(&:even?) + .first(5) + + assert_equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], out) + assert_equal([2, 4, 6, 8, 10], e) + end + + def test_tap_each_is_not_intrusive + s = Step.new(1..3) + + assert_equal(2, s.lazy.tap_each { |x| x }.map { |x| x * 2 }.first) + assert_equal(1, s.current) + end end diff --git a/test/ruby/test_literal.rb b/test/ruby/test_literal.rb index dbff3c4734..cff888d4b3 100644 --- a/test/ruby/test_literal.rb +++ b/test/ruby/test_literal.rb @@ -682,6 +682,11 @@ class TestRubyLiteral < Test::Unit::TestCase $VERBOSE = verbose_bak end + def test_rational_float + assert_equal(12, 0.12r * 100) + assert_equal(12, 0.1_2r * 100) + end + def test_symbol_list assert_equal([:foo, :bar], %i[foo bar]) assert_equal([:"\"foo"], %i["foo]) diff --git a/test/ruby/test_marshal.rb b/test/ruby/test_marshal.rb index eb66994801..48a67e1dc5 100644 --- a/test/ruby/test_marshal.rb +++ b/test/ruby/test_marshal.rb @@ -314,7 +314,7 @@ class TestMarshal < Test::Unit::TestCase def test_regexp2 assert_equal(/\\u/, Marshal.load("\004\b/\b\\\\u\000")) assert_equal(/u/, Marshal.load("\004\b/\a\\u\000")) - assert_equal(/u/, Marshal.load("\004\bI/\a\\u\000\006:\016@encoding\"\vEUC-JP")) + assert_raise(FrozenError) { Marshal.load("\x04\bI/\x06u\x00\a:\x06EF:\t@fooi/") } bug2109 = '[ruby-core:25625]' a = "\x82\xa0".force_encoding(Encoding::Windows_31J) @@ -471,7 +471,7 @@ class TestMarshal < Test::Unit::TestCase class TooComplex def initialize - @marshal_too_complex = 1 + @marshal_complex = 1 end end @@ -487,10 +487,10 @@ class TestMarshal < Test::Unit::TestCase obj.instance_variable_set(ivar, 1) if defined?(RubyVM::Shape) - assert_predicate(RubyVM::Shape.of(obj), :too_complex?) + assert_predicate(RubyVM::Shape.of(obj), :complex?) end obj.object_id - assert_equal "\x04\bo:\x1CTestMarshal::TooComplex\a:\x19@marshal_too_complexi\x06:\f#{ivar}i\x06".b, Marshal.dump(obj) + assert_equal "\x04\bo:\x1CTestMarshal::TooComplex\a:\x15@marshal_complexi\x06:\f#{ivar}i\x06".b, Marshal.dump(obj) end def test_marshal_complex @@ -859,17 +859,15 @@ class TestMarshal < Test::Unit::TestCase def test_marshal_dump_adding_instance_variable obj = Bug15968.new - assert_raise_with_message(RuntimeError, /instance variable added/) do - Marshal.dump(obj) - end + loaded = Marshal.load(Marshal.dump(obj)) + assert_nil loaded.baz end def test_marshal_dump_removing_instance_variable obj = Bug15968.new obj.baz = :Bug15968 - assert_raise_with_message(RuntimeError, /instance variable removed/) do - Marshal.dump(obj) - end + loaded = Marshal.load(Marshal.dump(obj)) + assert_equal :Bug15968, loaded.baz end ruby2_keywords def ruby2_keywords_hash(*a) @@ -935,6 +933,41 @@ class TestMarshal < Test::Unit::TestCase end end + def test_load_overread + input = Struct.new(:bytes, :used) do + def initialize + super("\x04\x08[\x07".bytes, false) + end + + def getbyte + bytes.shift + end + + def read(_len, _outbuf = nil) + return nil if used + self.used = true + "0" * (1024 * 128) + end + end.new + + assert_equal([nil, nil], Marshal.load(input)) + end + + def test_bignum_len_overflow + assert_raise(ArgumentError) do + Marshal.load("\x04\x08l+\x04\x00\x00\x00\x40") + end + assert_raise(ArgumentError) do + Marshal.load("\x04\x08l+\xfc\x00\x00\x00\x80") + end + end + + def test_bignum_invalid_sign + assert_raise(ArgumentError) do + Marshal.load("\x04\bl?") + end + end + class TestMarshalFreezeProc < Test::Unit::TestCase include MarshalTestLib @@ -988,7 +1021,7 @@ class TestMarshal < Test::Unit::TestCase end def test_proc_returned_object_are_not_frozen - source = ["foo", {}, /foo/, 1..2] + source = ["foo", {}, 1..2] objects = Marshal.load(encode(source), ->(o) { o.dup }, freeze: true) assert_equal source, objects refute_predicate objects, :frozen? @@ -1002,5 +1035,19 @@ class TestMarshal < Test::Unit::TestCase refute_predicate Object, :frozen? refute_predicate Kernel, :frozen? end + + def test_linked_strings_are_frozen + str = "test" + str.instance_variable_set(:@self, str) + source = [str, str] + + objects = Marshal.load(encode(source), freeze: true) + assert_predicate objects[0], :frozen? + assert_predicate objects[1], :frozen? + assert_same objects[0], objects[1] + assert_same objects[0], objects[0].instance_variable_get(:@self) + assert_same objects[1], objects[1].instance_variable_get(:@self) + assert_same objects[0].instance_variable_get(:@self), objects[1].instance_variable_get(:@self) + end end end diff --git a/test/ruby/test_math.rb b/test/ruby/test_math.rb index a676bb5cd9..e134600cc4 100644 --- a/test/ruby/test_math.rb +++ b/test/ruby/test_math.rb @@ -321,11 +321,21 @@ class TestMath < Test::Unit::TestCase assert_float_and_int([Math.log(6), 1], Math.lgamma(4)) assert_raise_with_message(Math::DomainError, /\blgamma\b/) { Math.lgamma(-Float::INFINITY) } + + x, sign = Math.lgamma(+0.0) + mesg = "Math.lgamma(+0.0) should be [INF, +1]" + assert_infinity(x, mesg) + assert_equal(+1, sign, mesg) + x, sign = Math.lgamma(-0.0) mesg = "Math.lgamma(-0.0) should be [INF, -1]" assert_infinity(x, mesg) assert_equal(-1, sign, mesg) - x, sign = Math.lgamma(Float::NAN) + + x, = Math.lgamma(-1) + assert_infinity(x, "Math.lgamma(-1) should be +INF") + + x, = Math.lgamma(Float::NAN) assert_nan(x) end diff --git a/test/ruby/test_metaclass.rb b/test/ruby/test_metaclass.rb index 8c1990a78c..6570fa5945 100644 --- a/test/ruby/test_metaclass.rb +++ b/test/ruby/test_metaclass.rb @@ -163,6 +163,6 @@ class TestMetaclass < Test::Unit::TestCase assert_nothing_raised{ metametaclass_of_bar.metaclass_method_c } assert_nothing_raised{ metametaclass_of_bar.metametaclass_method_o } assert_nothing_raised{ metametaclass_of_bar.metametaclass_method_f } - assert_raise(NoMethodError){ metametaclass_of_bar.metaclass_method_b } + assert_raise(NoMethodError){ metametaclass_of_bar.metametaclass_method_b } end end diff --git a/test/ruby/test_method.rb b/test/ruby/test_method.rb index 8561f841a8..00512bf2c6 100644 --- a/test/ruby/test_method.rb +++ b/test/ruby/test_method.rb @@ -32,6 +32,7 @@ class TestMethod < Test::Unit::TestCase def mk7(a, b = nil, *c, d, **o) nil && o end def mk8(a, b = nil, *c, d, e:, f: nil, **o) nil && o end def mnk(**nil) end + def mnb(&nil) end def mf(...) end class Base @@ -111,6 +112,20 @@ class TestMethod < Test::Unit::TestCase end end + def test_unbound_method_equality_with_extended_module + m = Module.new { def hello; "hello"; end } + base = Class.new { extend m } + sub = Class.new(base) + + from_module = m.instance_method(:hello) + from_base = base.method(:hello).unbind + from_sub = sub.method(:hello).unbind + + assert_equal(from_module, from_base) + assert_equal(from_module, from_sub) + assert_equal(from_base, from_sub) + end + def test_callee assert_equal(:test_callee, __method__) assert_equal(:m, Class.new {def m; __method__; end}.new.m) @@ -488,6 +503,20 @@ class TestMethod < Test::Unit::TestCase end end + def test_clone_preserves_singleton_methods + m = method(:itself) + m.define_singleton_method(:foo) { :bar } + assert_equal(:bar, m.foo) + assert_equal(:bar, m.clone.foo) + end + + def test_dup_does_not_preserve_singleton_methods + m = method(:itself) + m.define_singleton_method(:foo) { :bar } + assert_equal(:bar, m.foo) + assert_raise(NoMethodError) { m.dup.foo } + end + def test_inspect o = Object.new def o.foo; end; line_no = __LINE__ @@ -603,6 +632,7 @@ class TestMethod < Test::Unit::TestCase define_method(:pmk7) {|a, b = nil, *c, d, **o|} define_method(:pmk8) {|a, b = nil, *c, d, e:, f: nil, **o|} define_method(:pmnk) {|**nil|} + define_method(:pmnb) {|&nil|} def test_bound_parameters assert_equal([], method(:m0).parameters) @@ -626,6 +656,7 @@ class TestMethod < Test::Unit::TestCase assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyrest, :o]], method(:mk7).parameters) assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyreq, :e], [:key, :f], [:keyrest, :o]], method(:mk8).parameters) assert_equal([[:nokey]], method(:mnk).parameters) + assert_equal([[:noblock]], method(:mnb).parameters) # pending assert_equal([[:rest, :*], [:keyrest, :**], [:block, :&]], method(:mf).parameters) end @@ -652,6 +683,7 @@ class TestMethod < Test::Unit::TestCase assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyrest, :o]], self.class.instance_method(:mk7).parameters) assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyreq, :e], [:key, :f], [:keyrest, :o]], self.class.instance_method(:mk8).parameters) assert_equal([[:nokey]], self.class.instance_method(:mnk).parameters) + assert_equal([[:noblock]], self.class.instance_method(:mnb).parameters) # pending assert_equal([[:rest, :*], [:keyrest, :**], [:block, :&]], self.class.instance_method(:mf).parameters) end @@ -677,6 +709,7 @@ class TestMethod < Test::Unit::TestCase assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyrest, :o]], method(:pmk7).parameters) assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyreq, :e], [:key, :f], [:keyrest, :o]], method(:pmk8).parameters) assert_equal([[:nokey]], method(:pmnk).parameters) + assert_equal([[:noblock]], method(:pmnb).parameters) end def test_bmethod_unbound_parameters @@ -701,6 +734,7 @@ class TestMethod < Test::Unit::TestCase assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyrest, :o]], self.class.instance_method(:pmk7).parameters) assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyreq, :e], [:key, :f], [:keyrest, :o]], self.class.instance_method(:pmk8).parameters) assert_equal([[:nokey]], self.class.instance_method(:pmnk).parameters) + assert_equal([[:noblock]], self.class.instance_method(:pmnb).parameters) end def test_hidden_parameters diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index bc8583b475..ad83d09823 100644 --- a/test/ruby/test_module.rb +++ b/test/ruby/test_module.rb @@ -9,18 +9,18 @@ class TestModule < Test::Unit::TestCase yield end - def assert_method_defined?(klass, mid, message="") + def assert_method_defined?(klass, (mid, *args), message="") message = build_message(message, "#{klass}\##{mid} expected to be defined.") _wrap_assertion do - klass.method_defined?(mid) or + klass.method_defined?(mid, *args) or raise Test::Unit::AssertionFailedError, message, caller(3) end end - def assert_method_not_defined?(klass, mid, message="") + def assert_method_not_defined?(klass, (mid, *args), message="") message = build_message(message, "#{klass}\##{mid} expected to not be defined.") _wrap_assertion do - klass.method_defined?(mid) and + klass.method_defined?(mid, *args) and raise Test::Unit::AssertionFailedError, message, caller(3) end end @@ -412,19 +412,6 @@ class TestModule < Test::Unit::TestCase assert_equal([:MIXIN, :USER], User.constants.sort) end - def test_initialize_copy - mod = Module.new { define_method(:foo) {:first} } - klass = Class.new { include mod } - instance = klass.new - assert_equal(:first, instance.foo) - new_mod = Module.new { define_method(:foo) { :second } } - assert_raise(TypeError) do - mod.send(:initialize_copy, new_mod) - end - 4.times { GC.start } - assert_equal(:first, instance.foo) # [BUG] unreachable - end - def test_initialize_copy_empty m = Module.new do def x @@ -435,11 +422,6 @@ class TestModule < Test::Unit::TestCase assert_equal([:x], m.instance_methods) assert_equal([:@x], m.instance_variables) assert_equal([:X], m.constants) - assert_raise(TypeError) do - m.module_eval do - initialize_copy(Module.new) - end - end m = Class.new(Module) do def initialize_copy(other) @@ -601,7 +583,7 @@ class TestModule < Test::Unit::TestCase end def test_gc_prepend_chain - assert_separately([], <<-EOS) + assert_ruby_status([], <<-EOS) 10000.times { |i| m1 = Module.new do def foo; end @@ -831,40 +813,40 @@ class TestModule < Test::Unit::TestCase def test_method_defined? [User, Class.new{include User}, Class.new{prepend User}].each do |klass| [[], [true]].each do |args| - assert !klass.method_defined?(:wombat, *args) - assert klass.method_defined?(:mixin, *args) - assert klass.method_defined?(:user, *args) - assert klass.method_defined?(:user2, *args) - assert !klass.method_defined?(:user3, *args) + assert_method_not_defined?(klass, [:wombat, *args]) + assert_method_defined?(klass, [:mixin, *args]) + assert_method_defined?(klass, [:user, *args]) + assert_method_defined?(klass, [:user2, *args]) + assert_method_not_defined?(klass, [:user3, *args]) - assert !klass.method_defined?("wombat", *args) - assert klass.method_defined?("mixin", *args) - assert klass.method_defined?("user", *args) - assert klass.method_defined?("user2", *args) - assert !klass.method_defined?("user3", *args) + assert_method_not_defined?(klass, ["wombat", *args]) + assert_method_defined?(klass, ["mixin", *args]) + assert_method_defined?(klass, ["user", *args]) + assert_method_defined?(klass, ["user2", *args]) + assert_method_not_defined?(klass, ["user3", *args]) end end end def test_method_defined_without_include_super - assert User.method_defined?(:user, false) - assert !User.method_defined?(:mixin, false) - assert Mixin.method_defined?(:mixin, false) + assert_method_defined?(User, [:user, false]) + assert_method_not_defined?(User, [:mixin, false]) + assert_method_defined?(Mixin, [:mixin, false]) User.const_set(:FOO, c = Class.new) c.prepend(User) - assert !c.method_defined?(:user, false) + assert_method_not_defined?(c, [:user, false]) c.define_method(:user){} - assert c.method_defined?(:user, false) + assert_method_defined?(c, [:user, false]) - assert !c.method_defined?(:mixin, false) + assert_method_not_defined?(c, [:mixin, false]) c.define_method(:mixin){} - assert c.method_defined?(:mixin, false) + assert_method_defined?(c, [:mixin, false]) - assert !c.method_defined?(:userx, false) + assert_method_not_defined?(c, [:userx, false]) c.define_method(:userx){} - assert c.method_defined?(:userx, false) + assert_method_defined?(c, [:userx, false]) # cleanup User.class_eval do @@ -1452,6 +1434,7 @@ class TestModule < Test::Unit::TestCase c.instance_eval { attr_reader :"." } end + c = Class.new assert_equal([:a], c.class_eval { attr :a }) assert_equal([:b, :c], c.class_eval { attr :b, :c }) assert_equal([:d], c.class_eval { attr_reader :d }) @@ -1460,6 +1443,16 @@ class TestModule < Test::Unit::TestCase assert_equal([:h=, :i=], c.class_eval { attr_writer :h, :i }) assert_equal([:j, :j=], c.class_eval { attr_accessor :j }) assert_equal([:k, :k=, :l, :l=], c.class_eval { attr_accessor :k, :l }) + + c = Class.new + assert_equal([:a], c.class_eval { attr "a" }) + assert_equal([:b, :c], c.class_eval { attr "b", "c" }) + assert_equal([:d], c.class_eval { attr_reader "d" }) + assert_equal([:e, :f], c.class_eval { attr_reader "e", "f" }) + assert_equal([:g=], c.class_eval { attr_writer "g" }) + assert_equal([:h=, :i=], c.class_eval { attr_writer "h", "i" }) + assert_equal([:j, :j=], c.class_eval { attr_accessor "j" }) + assert_equal([:k, :k=, :l, :l=], c.class_eval { attr_accessor "k", "l" }) end def test_alias_method @@ -2829,7 +2822,7 @@ class TestModule < Test::Unit::TestCase b = a.dup b.new.a = 'B' - assert_equal 'A', a.new.a, '[ruby-core:17019]' + assert_equal 'B', a.new.a, '[ruby-core:17019] behaviour changed: cvar resolves through original CREF' end Bug6891 = '[ruby-core:47241]' @@ -3023,17 +3016,17 @@ class TestModule < Test::Unit::TestCase bug11532 = '[ruby-core:70828] [Bug #11532]' c = Class.new {const_set(:A, 1)}.freeze - assert_raise_with_message(FrozenError, /frozen class/, bug11532) { + assert_raise_with_message(FrozenError, /frozen Class/, bug11532) { c.class_eval {private_constant :A} } c = Class.new {const_set(:A, 1); private_constant :A}.freeze - assert_raise_with_message(FrozenError, /frozen class/, bug11532) { + assert_raise_with_message(FrozenError, /frozen Class/, bug11532) { c.class_eval {public_constant :A} } c = Class.new {const_set(:A, 1)}.freeze - assert_raise_with_message(FrozenError, /frozen class/, bug11532) { + assert_raise_with_message(FrozenError, /frozen Class/, bug11532) { c.class_eval {deprecate_constant :A} } end @@ -3080,7 +3073,7 @@ class TestModule < Test::Unit::TestCase end def test_prepend_gc - assert_separately [], %{ + assert_ruby_status [], %{ module Foo end class Object @@ -3272,15 +3265,18 @@ class TestModule < Test::Unit::TestCase end module CloneTestM0 + TEST = :M0 def foo; TEST; end end CloneTestM1 = CloneTestM0.clone CloneTestM2 = CloneTestM0.clone module CloneTestM1 + remove_const :TEST TEST = :M1 end module CloneTestM2 + remove_const :TEST TEST = :M2 end class CloneTestC1 @@ -3295,8 +3291,8 @@ class TestModule < Test::Unit::TestCase assert_equal 1, m::C, '[ruby-core:47834]' assert_equal 1, m.m, '[ruby-core:47834]' - assert_equal :M1, CloneTestC1.new.foo, '[Bug #15877]' - assert_equal :M2, CloneTestC2.new.foo, '[Bug #15877]' + assert_equal :M0, CloneTestC1.new.foo, 'originally [Bug #15877], but behaviour changed' + assert_equal :M0, CloneTestC2.new.foo, 'originally [Bug #15877], but behaviour changed' end def test_clone_freeze @@ -3374,11 +3370,11 @@ class TestModule < Test::Unit::TestCase m.const_set(:N, Module.new) assert_match(/\A#<Module:0x\h+>::N\z/, m::N.name) - m::N.set_temporary_name(name = "fake_name_under_M") + assert_same m::N, m::N.set_temporary_name(name = "fake_name_under_M") name.upcase! assert_equal("fake_name_under_M", m::N.name) assert_raise(FrozenError) {m::N.name.upcase!} - m::N.set_temporary_name(nil) + assert_same m::N, m::N.set_temporary_name(nil) assert_nil(m::N.name) m::N.const_set(:O, Module.new) @@ -3386,14 +3382,14 @@ class TestModule < Test::Unit::TestCase m::N.const_set(:Recursive, m) m.const_set(:A, 42) - m.set_temporary_name(name = "fake_name") + assert_same m, m.set_temporary_name(name = "fake_name") name.upcase! assert_equal("fake_name", m.name) assert_raise(FrozenError) {m.name.upcase!} assert_equal("fake_name::N", m::N.name) assert_equal("fake_name::N::O", m::N::O.name) - m.set_temporary_name(nil) + assert_same m, m.set_temporary_name(nil) assert_nil m.name assert_nil m::N.name assert_nil m::N::O.name diff --git a/test/ruby/test_namespace.rb b/test/ruby/test_namespace.rb deleted file mode 100644 index cd59306867..0000000000 --- a/test/ruby/test_namespace.rb +++ /dev/null @@ -1,618 +0,0 @@ -# frozen_string_literal: true - -require 'test/unit' - -class TestNamespace < Test::Unit::TestCase - ENV_ENABLE_NAMESPACE = {'RUBY_NAMESPACE' => '1'} - - def setup - @n = Namespace.new if Namespace.enabled? - end - - def teardown - @n = nil - end - - def test_namespace_availability - env_has_RUBY_NAMESPACE = (ENV['RUBY_NAMESPACE'].to_i == 1) - assert_equal env_has_RUBY_NAMESPACE, Namespace.enabled? - end - - def test_current_namespace - pend unless Namespace.enabled? - - main = Namespace.current - assert main.inspect.include?("main") - - @n.require_relative('namespace/current') - - assert_equal @n, @n::CurrentNamespace.in_require - assert_equal @n, @n::CurrentNamespace.in_method_call - assert_equal main, Namespace.current - end - - def test_require_rb_separately - pend unless Namespace.enabled? - - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - - @n.require(File.join(__dir__, 'namespace', 'a.1_1_0')) - - assert_not_nil @n::NS_A - assert_not_nil @n::NS_B - assert_equal "1.1.0", @n::NS_A::VERSION - assert_equal "yay 1.1.0", @n::NS_A.new.yay - assert_equal "1.1.0", @n::NS_B::VERSION - assert_equal "yay_b1", @n::NS_B.yay - - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - end - - def test_require_relative_rb_separately - pend unless Namespace.enabled? - - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - - @n.require_relative('namespace/a.1_1_0') - - assert_not_nil @n::NS_A - assert_not_nil @n::NS_B - assert_equal "1.1.0", @n::NS_A::VERSION - assert_equal "yay 1.1.0", @n::NS_A.new.yay - assert_equal "1.1.0", @n::NS_B::VERSION - assert_equal "yay_b1", @n::NS_B.yay - - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - end - - def test_load_separately - pend unless Namespace.enabled? - - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - - @n.load(File.join(__dir__, 'namespace', 'a.1_1_0.rb')) - - assert_not_nil @n::NS_A - assert_not_nil @n::NS_B - assert_equal "1.1.0", @n::NS_A::VERSION - assert_equal "yay 1.1.0", @n::NS_A.new.yay - assert_equal "1.1.0", @n::NS_B::VERSION - assert_equal "yay_b1", @n::NS_B.yay - - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - end - - def test_namespace_in_namespace - pend unless Namespace.enabled? - - assert_raise(NameError) { NS1 } - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - - @n.require_relative('namespace/ns') - - assert_not_nil @n::NS1 - assert_not_nil @n::NS1::NS_A - assert_not_nil @n::NS1::NS_B - assert_equal "1.1.0", @n::NS1::NS_A::VERSION - assert_equal "yay 1.1.0", @n::NS1::NS_A.new.yay - assert_equal "1.1.0", @n::NS1::NS_B::VERSION - assert_equal "yay_b1", @n::NS1::NS_B.yay - - assert_raise(NameError) { NS1 } - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - end - - def test_require_rb_2versions - pend unless Namespace.enabled? - - assert_raise(NameError) { NS_A } - - @n.require(File.join(__dir__, 'namespace', 'a.1_2_0')) - assert_equal "1.2.0", @n::NS_A::VERSION - assert_equal "yay 1.2.0", @n::NS_A.new.yay - - n2 = Namespace.new - n2.require(File.join(__dir__, 'namespace', 'a.1_1_0')) - assert_equal "1.1.0", n2::NS_A::VERSION - assert_equal "yay 1.1.0", n2::NS_A.new.yay - - # recheck @n is not affected by the following require - assert_equal "1.2.0", @n::NS_A::VERSION - assert_equal "yay 1.2.0", @n::NS_A.new.yay - - assert_raise(NameError) { NS_A } - end - - def test_raising_errors_in_require - pend unless Namespace.enabled? - - assert_raise(RuntimeError, "Yay!") { @n.require(File.join(__dir__, 'namespace', 'raise')) } - assert Namespace.current.inspect.include?("main") - end - - def test_autoload_in_namespace - pend unless Namespace.enabled? - - assert_raise(NameError) { NS_A } - - @n.require_relative('namespace/autoloading') - # autoloaded A is visible from global - assert_equal '1.1.0', @n::NS_A::VERSION - - assert_raise(NameError) { NS_A } - - # autoload trigger NS_B::BAR is valid even from global - assert_equal 'bar_b1', @n::NS_B::BAR - - assert_raise(NameError) { NS_A } - assert_raise(NameError) { NS_B } - end - - def test_continuous_top_level_method_in_a_namespace - pend unless Namespace.enabled? - - @n.require_relative('namespace/define_toplevel') - @n.require_relative('namespace/call_toplevel') - - assert_raise(NameError) { foo } - end - - def test_top_level_methods_in_namespace - pend # TODO: fix loading/current namespace detection - pend unless Namespace.enabled? - @n.require_relative('namespace/top_level') - assert_equal "yay!", @n::Foo.foo - assert_raise(NameError) { yaaay } - assert_equal "foo", @n::Bar.bar - assert_raise_with_message(RuntimeError, "boooo") { @n::Baz.baz } - end - - def test_proc_defined_in_namespace_refers_module_in_namespace - pend unless Namespace.enabled? - - # require_relative dosn't work well in assert_separately even with __FILE__ and __LINE__ - assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}") - begin; - ns1 = Namespace.new - ns1.require(File.join("#{here}", 'namespace/proc_callee')) - proc_v = ns1::Foo.callee - assert_raise(NameError) { Target } - assert ns1::Target - assert_equal "fooooo", proc_v.call # refers Target in the namespace ns1 - ns1.require(File.join("#{here}", 'namespace/proc_caller')) - assert_equal "fooooo", ns1::Bar.caller(proc_v) - - ns2 = Namespace.new - ns2.require(File.join("#{here}", 'namespace/proc_caller')) - assert_raise(NameError) { ns2::Target } - assert_equal "fooooo", ns2::Bar.caller(proc_v) # refers Target in the namespace ns1 - end; - end - - def test_proc_defined_globally_refers_global_module - pend unless Namespace.enabled? - - # require_relative dosn't work well in assert_separately even with __FILE__ and __LINE__ - assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) - begin; - require(File.join("#{here}", 'namespace/proc_callee')) - def Target.foo - "yay" - end - proc_v = Foo.callee - assert Target - assert_equal "yay", proc_v.call # refers global Foo - ns1 = Namespace.new - ns1.require(File.join("#{here}", 'namespace/proc_caller')) - assert_equal "yay", ns1::Bar.caller(proc_v) - - ns2 = Namespace.new - ns2.require(File.join("#{here}", 'namespace/proc_callee')) - ns2.require(File.join("#{here}", 'namespace/proc_caller')) - assert_equal "fooooo", ns2::Foo.callee.call - assert_equal "yay", ns2::Bar.caller(proc_v) # should refer the global Target, not Foo in ns2 - end; - end - - def test_instance_variable - pend unless Namespace.enabled? - - @n.require_relative('namespace/instance_variables') - - assert_equal [], String.instance_variables - assert_equal [:@str_ivar1, :@str_ivar2], @n::StringDelegatorObj.instance_variables - assert_equal 111, @n::StringDelegatorObj.str_ivar1 - assert_equal 222, @n::StringDelegatorObj.str_ivar2 - assert_equal 222, @n::StringDelegatorObj.instance_variable_get(:@str_ivar2) - - @n::StringDelegatorObj.instance_variable_set(:@str_ivar3, 333) - assert_equal 333, @n::StringDelegatorObj.instance_variable_get(:@str_ivar3) - @n::StringDelegatorObj.remove_instance_variable(:@str_ivar1) - assert_nil @n::StringDelegatorObj.str_ivar1 - assert_equal [:@str_ivar2, :@str_ivar3], @n::StringDelegatorObj.instance_variables - - assert_equal [], String.instance_variables - end - - def test_methods_added_in_namespace_are_invisible_globally - pend unless Namespace.enabled? - - @n.require_relative('namespace/string_ext') - - assert_equal "yay", @n::Bar.yay - - assert_raise(NoMethodError){ String.new.yay } - end - - def test_continuous_method_definitions_in_a_namespace - pend unless Namespace.enabled? - - @n.require_relative('namespace/string_ext') - assert_equal "yay", @n::Bar.yay - - @n.require_relative('namespace/string_ext_caller') - assert_equal "yay", @n::Foo.yay - - @n.require_relative('namespace/string_ext_calling') - end - - def test_methods_added_in_namespace_later_than_caller_code - pend unless Namespace.enabled? - - @n.require_relative('namespace/string_ext_caller') - @n.require_relative('namespace/string_ext') - - assert_equal "yay", @n::Bar.yay - assert_equal "yay", @n::Foo.yay - end - - def test_method_added_in_namespace_are_available_on_eval - pend unless Namespace.enabled? - - @n.require_relative('namespace/string_ext') - @n.require_relative('namespace/string_ext_eval_caller') - - assert_equal "yay", @n::Baz.yay - end - - def test_method_added_in_namespace_are_available_on_eval_with_binding - pend unless Namespace.enabled? - - @n.require_relative('namespace/string_ext') - @n.require_relative('namespace/string_ext_eval_caller') - - assert_equal "yay, yay!", @n::Baz.yay_with_binding - end - - def test_methods_and_constants_added_by_include - pend unless Namespace.enabled? - - @n.require_relative('namespace/open_class_with_include') - - assert_equal "I'm saying foo 1", @n::OpenClassWithInclude.say - assert_equal "I'm saying foo 1", @n::OpenClassWithInclude.say_foo - assert_equal "I'm saying foo 1", @n::OpenClassWithInclude.say_with_obj("wow") - - assert_raise(NameError) { String::FOO } - - assert_equal "foo 1", @n::OpenClassWithInclude.refer_foo - end -end - -module ProcLookupTestA - module B - VALUE = 111 - end -end - -class TestNamespace < Test::Unit::TestCase - def make_proc_from_block(&b) - b - end - - def test_proc_from_main_works_with_global_definitions - pend unless Namespace.enabled? - - @n.require_relative('namespace/procs') - - proc_and_labels = [ - [Proc.new { String.new.yay }, "Proc.new"], - [proc { String.new.yay }, "proc{}"], - [lambda { String.new.yay }, "lambda{}"], - [->(){ String.new.yay }, "->(){}"], - [make_proc_from_block { String.new.yay }, "make_proc_from_block"], - [@n::ProcInNS.make_proc_from_block { String.new.yay }, "make_proc_from_block in @n"], - ] - - proc_and_labels.each do |str_pr| - pr, pr_label = str_pr - assert_raise(NoMethodError, "NoMethodError expected: #{pr_label}, called in main") { pr.call } - assert_raise(NoMethodError, "NoMethodError expected: #{pr_label}, called in @n") { @n::ProcInNS.call_proc(pr) } - end - - const_and_labels = [ - [Proc.new { ProcLookupTestA::B::VALUE }, "Proc.new"], - [proc { ProcLookupTestA::B::VALUE }, "proc{}"], - [lambda { ProcLookupTestA::B::VALUE }, "lambda{}"], - [->(){ ProcLookupTestA::B::VALUE }, "->(){}"], - [make_proc_from_block { ProcLookupTestA::B::VALUE }, "make_proc_from_block"], - [@n::ProcInNS.make_proc_from_block { ProcLookupTestA::B::VALUE }, "make_proc_from_block in @n"], - ] - - const_and_labels.each do |const_pr| - pr, pr_label = const_pr - assert_equal 111, pr.call, "111 expected, #{pr_label} called in main" - assert_equal 111, @n::ProcInNS.call_proc(pr), "111 expected, #{pr_label} called in @n" - end - end - - def test_proc_from_namespace_works_with_definitions_in_namespace - pend unless Namespace.enabled? - - @n.require_relative('namespace/procs') - - proc_types = [:proc_new, :proc_f, :lambda_f, :lambda_l, :block] - - proc_types.each do |proc_type| - assert_equal 222, @n::ProcInNS.make_const_proc(proc_type).call, "ProcLookupTestA::B::VALUE should be 222 in @n" - assert_equal "foo", @n::ProcInNS.make_str_const_proc(proc_type).call, "String::FOO should be \"foo\" in @n" - assert_equal "yay", @n::ProcInNS.make_str_proc(proc_type).call, "String#yay should be callable in @n" - # - # TODO: method calls not-in-methods nor procs can't handle the current namespace correctly. - # - # assert_equal "yay,foo,222", - # @n::ProcInNS.const_get(('CONST_' + proc_type.to_s.upcase).to_sym).call, - # "Proc assigned to constants should refer constants correctly in @n" - end - end - - def test_class_module_singleton_methods - pend unless Namespace.enabled? - - @n.require_relative('namespace/singleton_methods') - - assert_equal "Good evening!", @n::SingletonMethods.string_greeing # def self.greeting - assert_equal 42, @n::SingletonMethods.integer_answer # class << self; def answer - assert_equal([], @n::SingletonMethods.array_blank) # def self.blank w/ instance methods - assert_equal({status: 200, body: 'OK'}, @n::SingletonMethods.hash_http_200) # class << self; def ... w/ instance methods - - assert_equal([4, 4], @n::SingletonMethods.array_instance_methods_return_size([1, 2, 3, 4])) - assert_equal([3, 3], @n::SingletonMethods.hash_instance_methods_return_size({a: 2, b: 4, c: 8})) - - assert_raise(NoMethodError) { String.greeting } - assert_raise(NoMethodError) { Integer.answer } - assert_raise(NoMethodError) { Array.blank } - assert_raise(NoMethodError) { Hash.http_200 } - end - - def test_add_constants_in_namespace - pend unless Namespace.enabled? - - String.const_set(:STR_CONST0, 999) - assert_equal 999, String::STR_CONST0 - assert_equal 999, String.const_get(:STR_CONST0) - - assert_raise(NameError) { String.const_get(:STR_CONST1) } - assert_raise(NameError) { String::STR_CONST2 } - assert_raise(NameError) { String::STR_CONST3 } - assert_raise(NameError) { Integer.const_get(:INT_CONST1) } - - EnvUtil.suppress_warning do - @n.require_relative('namespace/consts') - end - assert_equal 999, String::STR_CONST0 - assert_raise(NameError) { String::STR_CONST1 } - assert_raise(NameError) { String::STR_CONST2 } - assert_raise(NameError) { Integer::INT_CONST1 } - - assert_not_nil @n::ForConsts.refer_all - - assert_equal 112, @n::ForConsts.refer1 - assert_equal 112, @n::ForConsts.get1 - assert_equal 112, @n::ForConsts::CONST1 - assert_equal 222, @n::ForConsts.refer2 - assert_equal 222, @n::ForConsts.get2 - assert_equal 222, @n::ForConsts::CONST2 - assert_equal 333, @n::ForConsts.refer3 - assert_equal 333, @n::ForConsts.get3 - assert_equal 333, @n::ForConsts::CONST3 - - EnvUtil.suppress_warning do - @n::ForConsts.const_set(:CONST3, 334) - end - assert_equal 334, @n::ForConsts::CONST3 - assert_equal 334, @n::ForConsts.refer3 - assert_equal 334, @n::ForConsts.get3 - - assert_equal 10, @n::ForConsts.refer_top_const - - # use Proxy object to use usual methods instead of singleton methods - proxy = @n::ForConsts::Proxy.new - - assert_raise(NameError){ proxy.call_str_refer0 } - assert_raise(NameError){ proxy.call_str_get0 } - - proxy.call_str_set0(30) - assert_equal 30, proxy.call_str_refer0 - assert_equal 30, proxy.call_str_get0 - assert_equal 999, String::STR_CONST0 - - proxy.call_str_remove0 - assert_raise(NameError){ proxy.call_str_refer0 } - assert_raise(NameError){ proxy.call_str_get0 } - - assert_equal 112, proxy.call_str_refer1 - assert_equal 112, proxy.call_str_get1 - assert_equal 223, proxy.call_str_refer2 - assert_equal 223, proxy.call_str_get2 - assert_equal 333, proxy.call_str_refer3 - assert_equal 333, proxy.call_str_get3 - - EnvUtil.suppress_warning do - proxy.call_str_set3 - end - assert_equal 334, proxy.call_str_refer3 - assert_equal 334, proxy.call_str_get3 - - assert_equal 1, proxy.refer_int_const1 - - assert_equal 999, String::STR_CONST0 - assert_raise(NameError) { String::STR_CONST1 } - assert_raise(NameError) { String::STR_CONST2 } - assert_raise(NameError) { String::STR_CONST3 } - assert_raise(NameError) { Integer::INT_CONST1 } - end - - def test_global_variables - default_l = $-0 - default_f = $, - - pend unless Namespace.enabled? - - assert_equal "\n", $-0 # equal to $/, line splitter - assert_equal nil, $, # field splitter - - @n.require_relative('namespace/global_vars') - - # read first - assert_equal "\n", @n::LineSplitter.read - @n::LineSplitter.write("\r\n") - assert_equal "\r\n", @n::LineSplitter.read - assert_equal "\n", $-0 - - # write first - @n::FieldSplitter.write(",") - assert_equal ",", @n::FieldSplitter.read - assert_equal nil, $, - - # used only in ns - assert !global_variables.include?(:$used_only_in_ns) - @n::UniqueGvar.write(123) - assert_equal 123, @n::UniqueGvar.read - assert_nil $used_only_in_ns - - # Kernel#global_variables returns the sum of all gvars. - global_gvars = global_variables.sort - assert_equal global_gvars, @n::UniqueGvar.gvars_in_ns.sort - @n::UniqueGvar.write_only(456) - assert_equal (global_gvars + [:$write_only_var_in_ns]).sort, @n::UniqueGvar.gvars_in_ns.sort - assert_equal (global_gvars + [:$write_only_var_in_ns]).sort, global_variables.sort - ensure - EnvUtil.suppress_warning do - $-0 = default_l - $, = default_f - end - end - - def test_load_path_and_loaded_features - pend unless Namespace.enabled? - - assert $LOAD_PATH.respond_to?(:resolve_feature_path) - - @n.require_relative('namespace/load_path') - - assert_not_equal $LOAD_PATH, @n::LoadPathCheck::FIRST_LOAD_PATH - - assert @n::LoadPathCheck::FIRST_LOAD_PATH_RESPOND_TO_RESOLVE - - namespace_dir = File.join(__dir__, 'namespace') - # TODO: $LOADED_FEATURES in method calls should refer the current namespace in addition to the loading namespace. - # assert @n::LoadPathCheck.current_loaded_features.include?(File.join(namespace_dir, 'blank1.rb')) - # assert !@n::LoadPathCheck.current_loaded_features.include?(File.join(namespace_dir, 'blank2.rb')) - # assert @n::LoadPathCheck.require_blank2 - # assert @n::LoadPathCheck.current_loaded_features.include?(File.join(namespace_dir, 'blank2.rb')) - - assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank1.rb')) - assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank2.rb')) - end - - def test_eval_basic - pend unless Namespace.enabled? - - # Test basic evaluation - result = @n.eval("1 + 1") - assert_equal 2, result - - # Test string evaluation - result = @n.eval("'hello ' + 'world'") - assert_equal "hello world", result - end - - def test_eval_with_constants - pend unless Namespace.enabled? - - # Define a constant in the namespace via eval - @n.eval("TEST_CONST = 42") - assert_equal 42, @n::TEST_CONST - - # Constant should not be visible in main namespace - assert_raise(NameError) { TEST_CONST } - end - - def test_eval_with_classes - pend unless Namespace.enabled? - - # Define a class in the namespace via eval - @n.eval("class TestClass; def hello; 'from namespace'; end; end") - - # Class should be accessible in the namespace - instance = @n::TestClass.new - assert_equal "from namespace", instance.hello - - # Class should not be visible in main namespace - assert_raise(NameError) { TestClass } - end - - def test_eval_isolation - pend unless Namespace.enabled? - - # Create another namespace - n2 = Namespace.new - - # Define different constants in each namespace - @n.eval("ISOLATION_TEST = 'first'") - n2.eval("ISOLATION_TEST = 'second'") - - # Each namespace should have its own constant - assert_equal "first", @n::ISOLATION_TEST - assert_equal "second", n2::ISOLATION_TEST - - # Constants should not interfere with each other - assert_not_equal @n::ISOLATION_TEST, n2::ISOLATION_TEST - end - - def test_eval_with_variables - pend unless Namespace.enabled? - - # Test local variable access (should work within the eval context) - result = @n.eval("x = 10; y = 20; x + y") - assert_equal 30, result - end - - def test_eval_error_handling - pend unless Namespace.enabled? - - # Test syntax error - assert_raise(SyntaxError) { @n.eval("1 +") } - - # Test name error - assert_raise(NameError) { @n.eval("undefined_variable") } - - # Test that namespace is properly restored after error - begin - @n.eval("raise RuntimeError, 'test error'") - rescue RuntimeError - # Should be able to continue using the namespace - result = @n.eval("2 + 2") - assert_equal 4, result - end - end -end diff --git a/test/ruby/test_nomethod_error.rb b/test/ruby/test_nomethod_error.rb index 6d413e6391..6abd20cc81 100644 --- a/test/ruby/test_nomethod_error.rb +++ b/test/ruby/test_nomethod_error.rb @@ -78,7 +78,7 @@ class TestNoMethodError < Test::Unit::TestCase assert_equal :foo, error.name assert_equal [1, 2], error.args assert_equal receiver, error.receiver - assert error.private_call?, "private_call? was false." + assert_predicate error, :private_call? end def test_message_encoding @@ -106,4 +106,32 @@ class TestNoMethodError < Test::Unit::TestCase assert_match(/undefined method.+this_method_does_not_exist.+for.+Module/, err.to_s) end + + def test_send_forward_raises + t = EnvUtil.labeled_class("Test") do + def foo(...) + forward(...) + end + end + obj = t.new + assert_raise(NoMethodError) do + obj.foo + end + end + + # [Bug #21535] + def test_send_forward_raises_when_called_through_vcall + t = EnvUtil.labeled_class("Test") do + def foo(...) + forward(...) + end + def foo_indirect + foo # vcall + end + end + obj = t.new + assert_raise(NoMethodError) do + obj.foo_indirect + end + end end diff --git a/test/ruby/test_numeric.rb b/test/ruby/test_numeric.rb index 3bf93ef20d..b272b89921 100644 --- a/test/ruby/test_numeric.rb +++ b/test/ruby/test_numeric.rb @@ -206,14 +206,6 @@ class TestNumeric < Test::Unit::TestCase assert_nil(a <=> :foo) end - def test_float_round_ndigits - bug14635 = "[ruby-core:86323]" - f = 0.5 - 31.times do |i| - assert_equal(0.5, f.round(i+1), bug14635 + " (argument: #{i+1})") - end - end - def test_floor_ceil_round_truncate a = Class.new(Numeric) do def to_f; 1.5; end @@ -489,6 +481,10 @@ class TestNumeric < Test::Unit::TestCase assert_equal(0, 0.pow(3, 1)) assert_equal(0, 2.pow(3, 1)) assert_equal(0, -2.pow(3, 1)) + + min, max = RbConfig::LIMITS.values_at("FIXNUM_MIN", "FIXNUM_MAX") + assert_equal(0, 0.pow(2, min)) + assert_equal(0, Integer.sqrt(max+1).pow(2, min)) end end diff --git a/test/ruby/test_object.rb b/test/ruby/test_object.rb index 9074e54df5..53ae4fb110 100644 --- a/test/ruby/test_object.rb +++ b/test/ruby/test_object.rb @@ -280,6 +280,12 @@ class TestObject < Test::Unit::TestCase assert_equal([:foo], k.private_methods(false)) end + class ToStrCounter + def initialize(str = "@foo") @str = str; @count = 0; end + def to_str; @count += 1; @str; end + def count; @count; end + end + def test_instance_variable_get o = Object.new o.instance_eval { @foo = :foo } @@ -291,9 +297,7 @@ class TestObject < Test::Unit::TestCase assert_raise(NameError) { o.instance_variable_get("bar") } assert_raise(TypeError) { o.instance_variable_get(1) } - n = Object.new - def n.to_str; @count = defined?(@count) ? @count + 1 : 1; "@foo"; end - def n.count; @count; end + n = ToStrCounter.new assert_equal(:foo, o.instance_variable_get(n)) assert_equal(1, n.count) end @@ -308,9 +312,7 @@ class TestObject < Test::Unit::TestCase assert_raise(NameError) { o.instance_variable_set("bar", 1) } assert_raise(TypeError) { o.instance_variable_set(1, 1) } - n = Object.new - def n.to_str; @count = defined?(@count) ? @count + 1 : 1; "@foo"; end - def n.count; @count; end + n = ToStrCounter.new o.instance_variable_set(n, :bar) assert_equal(:bar, o.instance_eval { @foo }) assert_equal(1, n.count) @@ -327,9 +329,7 @@ class TestObject < Test::Unit::TestCase assert_raise(NameError) { o.instance_variable_defined?("bar") } assert_raise(TypeError) { o.instance_variable_defined?(1) } - n = Object.new - def n.to_str; @count = defined?(@count) ? @count + 1 : 1; "@foo"; end - def n.count; @count; end + n = ToStrCounter.new assert_equal(true, o.instance_variable_defined?(n)) assert_equal(1, n.count) end @@ -356,38 +356,43 @@ class TestObject < Test::Unit::TestCase end def test_remove_instance_variable_re_embed - require "objspace" - - c = Class.new do - def a = @a - - def b = @b - - def c = @c - end - - o1 = c.new - o2 = c.new - - o1.instance_variable_set(:@foo, 5) - o1.instance_variable_set(:@a, 0) - o1.instance_variable_set(:@b, 1) - o1.instance_variable_set(:@c, 2) - refute_includes ObjectSpace.dump(o1), '"embedded":true' - o1.remove_instance_variable(:@foo) - assert_includes ObjectSpace.dump(o1), '"embedded":true' - - o2.instance_variable_set(:@a, 0) - o2.instance_variable_set(:@b, 1) - o2.instance_variable_set(:@c, 2) - assert_includes ObjectSpace.dump(o2), '"embedded":true' - - assert_equal(0, o1.a) - assert_equal(1, o1.b) - assert_equal(2, o1.c) - assert_equal(0, o2.a) - assert_equal(1, o2.b) - assert_equal(2, o2.c) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # Determine the RVALUE pool's embed capacity from GC constants. + rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + rbasic_size = GC::INTERNAL_CONSTANTS[:RBASIC_SIZE] + embed_cap = (rvalue_size - rbasic_size) / RbConfig::SIZEOF["void*"] + + # Build a class whose initialize sets embed_cap ivars so objects + # are allocated in the RVALUE pool with embedded storage. + init_body = embed_cap.times.map { |i| "@v#{i} = nil" }.join("; ") + c = Class.new { class_eval("def initialize; #{init_body}; end") } + + o1 = c.new + o2 = c.new + + # All embed_cap ivars fit - should be embedded + embed_cap.times { |i| o1.instance_variable_set(:"@v#{i}", i) } + assert_includes ObjectSpace.dump(o1), '"embedded":true' + + # One more ivar overflows embed capacity + o1.instance_variable_set(:@overflow, 99) + refute_includes ObjectSpace.dump(o1), '"embedded":true' + + # Remove the overflow ivar - should re-embed + o1.remove_instance_variable(:@overflow) + assert_includes ObjectSpace.dump(o1), '"embedded":true' + + # An object that never overflowed is also embedded + embed_cap.times { |i| o2.instance_variable_set(:"@v#{i}", i) } + assert_includes ObjectSpace.dump(o2), '"embedded":true' + + # Verify values survived re-embedding + embed_cap.times do |i| + assert_equal(i, o1.instance_variable_get(:"@v#{i}")) + assert_equal(i, o2.instance_variable_get(:"@v#{i}")) + end + end; end def test_convert_string @@ -965,6 +970,69 @@ class TestObject < Test::Unit::TestCase assert_not_include(s, "@password=") end + def test_inspect_mutating_ivar + obj = Object.new + evil = Object.new + evil.define_singleton_method(:inspect) do + obj.instance_variables.each { |v| obj.remove_instance_variable(v) } + "evil" + end + obj.instance_variable_set(:@evil, evil) + 10.times { |i| obj.instance_variable_set(:"@v#{i}", 0) } + # Buffered iteration: inspect sees a snapshot of the original ivars + result = obj.inspect + assert_include result, "@evil=evil" + 10.times { |i| assert_include result, "@v#{i}=0" } + end + + def test_inspect_mutating_ivar_complex + # Force complex by creating many shape variations on the same class + c = Class.new + 50.times do |i| + o = c.new + o.instance_variable_set(:"@unique_#{i}", 0) + end + + obj = c.new + evil = Object.new + evil.define_singleton_method(:inspect) do + obj.instance_variables.each { |v| obj.remove_instance_variable(v) } + "" + end + obj.instance_variable_set(:@evil, evil) + 10.times { |i| obj.instance_variable_set(:"@v#{i}", 0) } + # complex objects use st_foreach which handles mutation gracefully + obj.inspect + end + + def test_inspect_complex + kernel_inspect = Kernel.instance_method(:inspect) + + klasses = [ + Class.new, + Class.new(String), + Class.new(Array), + Class.new(Hash), + Struct.new(:x), + Class.new(Thread::Mutex), + # It's very difficult to get a complex T_CLASS, so that isn't tested here + ] + + klasses.each_with_index do |klass, idx| + 8.times do |i| + klass.new.instance_variable_set(:"@sib_#{rand(999999)}", 1) + end + + obj = klass.new + obj.instance_variable_set(:@a, 1) + obj.instance_variable_set(:@b, 2) + + s = kernel_inspect.bind_call(obj) + assert_include(s, "@a=1") + assert_include(s, "@b=2") + end + end + def test_singleton_methods assert_equal([], Object.new.singleton_methods) assert_equal([], Object.new.singleton_methods(false)) @@ -1022,6 +1090,47 @@ class TestObject < Test::Unit::TestCase assert_predicate(ys, :frozen?, '[Bug #19169]') end + def test_singleton_class_of_singleton_class_freeze + x = Object.new + xs = x.singleton_class + xxs = xs.singleton_class + xxxs = xxs.singleton_class + x.freeze + assert_predicate(xs, :frozen?, '[Bug #20319]') + assert_predicate(xxs, :frozen?, '[Bug #20319]') + assert_predicate(xxxs, :frozen?, '[Bug #20319]') + + y = Object.new + ys = y.singleton_class + ys.prepend(Module.new) + yys = ys.singleton_class + yys.prepend(Module.new) + yyys = yys.singleton_class + yyys.prepend(Module.new) + y.freeze + assert_predicate(ys, :frozen?, '[Bug #20319]') + assert_predicate(yys, :frozen?, '[Bug #20319]') + assert_predicate(yyys, :frozen?, '[Bug #20319]') + + c = Class.new + cs = c.singleton_class + ccs = cs.singleton_class + cccs = ccs.singleton_class + d = Class.new(c) + ds = d.singleton_class + dds = ds.singleton_class + ddds = dds.singleton_class + d.freeze + assert_predicate(d, :frozen?, '[Bug #20319]') + assert_predicate(ds, :frozen?, '[Bug #20319]') + assert_predicate(dds, :frozen?, '[Bug #20319]') + assert_predicate(ddds, :frozen?, '[Bug #20319]') + assert_not_predicate(c, :frozen?, '[Bug #20319]') + assert_not_predicate(cs, :frozen?, '[Bug #20319]') + assert_not_predicate(ccs, :frozen?, '[Bug #20319]') + assert_not_predicate(cccs, :frozen?, '[Bug #20319]') + end + def test_redef_method_missing bug5473 = '[ruby-core:40287]' ['ArgumentError.new("bug5473")', 'ArgumentError, "bug5473"', '"bug5473"'].each do |code| diff --git a/test/ruby/test_object_id.rb b/test/ruby/test_object_id.rb index adb819febc..034674e5be 100644 --- a/test/ruby/test_object_id.rb +++ b/test/ruby/test_object_id.rb @@ -140,7 +140,7 @@ end class TestObjectIdTooComplex < TestObjectId class TooComplex def initialize - @too_complex_obj_id_test = 1 + @complex_obj_id_test = 1 end end @@ -155,7 +155,7 @@ class TestObjectIdTooComplex < TestObjectId @obj.instance_variable_set("@a#{rand(10_000)}", 1) if defined?(RubyVM::Shape) - assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + assert_predicate(RubyVM::Shape.of(@obj), :complex?) end end end @@ -181,7 +181,7 @@ class TestObjectIdTooComplexClass < TestObjectId @obj.instance_variable_set("@test", 1) if defined?(RubyVM::Shape) - assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + assert_predicate(RubyVM::Shape.of(@obj), :complex?) end end end @@ -202,7 +202,7 @@ class TestObjectIdTooComplexGeneric < TestObjectId @obj.instance_variable_set("@a#{rand(10_000)}", 1) if defined?(RubyVM::Shape) - assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + assert_predicate(RubyVM::Shape.of(@obj), :complex?) end end end @@ -282,7 +282,7 @@ end class TestObjectIdStructTooComplex < TestObjectId StructTooComplex = Struct.new(:a) do def initialize - @too_complex_obj_id_test = 1 + @complex_obj_id_test = 1 end end @@ -297,7 +297,7 @@ class TestObjectIdStructTooComplex < TestObjectId @obj.instance_variable_set("@a#{rand(10_000)}", 1) if defined?(RubyVM::Shape) - assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + assert_predicate(RubyVM::Shape.of(@obj), :complex?) end end end diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index e39eafa5e5..1554b43f18 100644 --- a/test/ruby/test_optimization.rb +++ b/test/ruby/test_optimization.rb @@ -606,11 +606,11 @@ class TestRubyOptimization < Test::Unit::TestCase end class Bug10557 - def [](_) + def [](_, &) block_given? end - def []=(_, _) + def []=(_, _, &) block_given? end end @@ -728,7 +728,7 @@ class TestRubyOptimization < Test::Unit::TestCase insn = iseq.disasm assert_match %r{putobject\s+#{Regexp.quote('"1.8.0"..."1.8.8"')}}, insn assert_match %r{putobject\s+#{Regexp.quote('"2.0.0".."2.3.2"')}}, insn - assert_no_match(/putstring/, insn) + assert_no_match(/dupstring/, insn) assert_no_match(/newrange/, insn) end end @@ -946,14 +946,14 @@ class TestRubyOptimization < Test::Unit::TestCase end def test_peephole_optimization_without_trace - assert_separately [], <<-END + assert_ruby_status [], <<-END RubyVM::InstructionSequence.compile_option = {trace_instruction: false} eval "def foo; 1.times{|(a), &b| nil && a}; end" END end def test_clear_unreachable_keyword_args - assert_separately [], <<-END, timeout: 60 + assert_ruby_status [], <<-END, timeout: 60 script = <<-EOS if true else @@ -1080,7 +1080,7 @@ class TestRubyOptimization < Test::Unit::TestCase class Objtostring end - def test_objtostring + def test_objtostring_immediate assert_raise(NoMethodError){"#{BasicObject.new}"} assert_redefine_method('Symbol', 'to_s', <<-'end') assert_match %r{\A#<Symbol:0x[0-9a-f]+>\z}, "#{:foo}" @@ -1094,11 +1094,17 @@ class TestRubyOptimization < Test::Unit::TestCase assert_redefine_method('FalseClass', 'to_s', <<-'end') assert_match %r{\A#<FalseClass:0x[0-9a-f]+>\z}, "#{false}" end + end + + def test_objtostring_fixnum assert_redefine_method('Integer', 'to_s', <<-'end') (-1..10).each { |i| assert_match %r{\A#<Integer:0x[0-9a-f]+>\z}, "#{i}" } end + end + + def test_objtostring assert_equal "TestRubyOptimization::Objtostring", "#{Objtostring}" assert_match %r{\A#<Class:0x[0-9a-f]+>\z}, "#{Class.new}" assert_match %r{\A#<Module:0x[0-9a-f]+>\z}, "#{Module.new}" @@ -1256,6 +1262,9 @@ class TestRubyOptimization < Test::Unit::TestCase insn = iseq.disasm assert_match(/opt_new/, insn) assert_match(/OptNewFoo:.+@a=1, @b=2/, iseq.eval.inspect) + # clean up to avoid warnings + Object.send :remove_const, :OptNewFoo + Object.remove_method :optnew_foo if defined?(optnew_foo) end [ 'def optnew_foo(&) = OptNewFoo.new(&)', diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index ca089f09c3..6e5f0fe7ff 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -283,6 +283,15 @@ class TestPack < Test::Unit::TestCase assert_equal(["foo "], "foo ".unpack("a4")) assert_equal(["foo"], "foo".unpack("A4")) assert_equal(["foo"], "foo".unpack("a4")) + + assert_equal(["foo", 4], "foo\0 ".unpack("A4^")) + assert_equal(["foo\0", 4], "foo\0 ".unpack("a4^")) + assert_equal(["foo", 4], "foo ".unpack("A4^")) + assert_equal(["foo ", 4], "foo ".unpack("a4^")) + assert_equal(["foo", 3], "foo".unpack("A4^")) + assert_equal(["foo", 3], "foo".unpack("a4^")) + assert_equal(["foo", 6], "foo\0 ".unpack("A*^")) + assert_equal(["foo", 6], "foo ".unpack("A*^")) end def test_pack_unpack_Z @@ -298,6 +307,11 @@ class TestPack < Test::Unit::TestCase assert_equal(["foo"], "foo".unpack("Z*")) assert_equal(["foo"], "foo\0".unpack("Z*")) assert_equal(["foo"], "foo".unpack("Z5")) + + assert_equal(["foo", 3], "foo".unpack("Z*^")) + assert_equal(["foo", 4], "foo\0".unpack("Z*^")) + assert_equal(["foo", 3], "foo".unpack("Z5^")) + assert_equal(["foo", 5], "foo\0\0\0".unpack("Z5^")) end def test_pack_unpack_bB @@ -549,6 +563,8 @@ class TestPack < Test::Unit::TestCase assert_equal([0, 2], "\x00\x00\x02".unpack("CxC")) assert_raise(ArgumentError) { "".unpack("x") } + + assert_equal([0, 1, 2, 2, 3], "\x00\x00\x02".unpack("C^x^C^")) end def test_pack_unpack_X @@ -558,6 +574,7 @@ class TestPack < Test::Unit::TestCase assert_equal([0, 2, 2], "\x00\x02".unpack("CCXC")) assert_raise(ArgumentError) { "".unpack("X") } + assert_equal([0, 1, 2, 2, 1, 2, 2], "\x00\x02".unpack("C^C^X^C^")) end def test_pack_unpack_atmark @@ -571,6 +588,17 @@ class TestPack < Test::Unit::TestCase pos = RbConfig::LIMITS["UINTPTR_MAX"] - 99 # -100 assert_raise(RangeError) {"0123456789".unpack("@#{pos}C10")} + + assert_equal([1, 3, 4], "\x01\x00\x00\x02".unpack("x^@3^x^")) + end + + def test_unpack_carret + assert_equal([0], "abc".unpack("^")) + assert_equal([2], "abc".unpack("^", offset: 2)) + assert_equal([97, nil, 1], "a".unpack("CC^")) + + assert_raise(ArgumentError) { "".unpack("^!") } + assert_raise(ArgumentError) { "".unpack("^_") } end def test_pack_unpack_percent @@ -853,6 +881,19 @@ EXPECTED assert_equal "\xDE\xAD\xBE\xEF\xBA\xBE\xF0\x0D\0\0\xBA\xAD\xFA\xCE", buf assert_equal addr, [buf].pack('p') + + assert_packing_buffer_fail("b*") + assert_packing_buffer_fail("B*") + assert_packing_buffer_fail("h*") + assert_packing_buffer_fail("H*") + assert_packing_buffer_fail("u", 16384) + assert_packing_buffer_fail("m", 16384) + assert_packing_buffer_fail("M", 16384) + end + + def assert_packing_buffer_fail(fmt, size = 8192) + s = "\x01".b * size + assert_raise(ArgumentError) {[s].pack(fmt, buffer: s)} end def test_unpack_with_block @@ -872,27 +913,29 @@ EXPECTED def test_unpack1_offset assert_equal 65, "ZA".unpack1("C", offset: 1) + assert_equal 65, "ZA".unpack1("C", offset: -1) assert_equal "01000001", "YZA".unpack1("B*", offset: 2) assert_nil "abc".unpack1("C", offset: 3) - assert_raise_with_message(ArgumentError, /offset can't be negative/) { - "a".unpack1("C", offset: -1) - } assert_raise_with_message(ArgumentError, /offset outside of string/) { "a".unpack1("C", offset: 2) } + assert_raise_with_message(ArgumentError, /offset outside of string/) { + "a".unpack1("C", offset: -2) + } assert_nil "a".unpack1("C", offset: 1) end def test_unpack_offset assert_equal [65], "ZA".unpack("C", offset: 1) + assert_equal [65], "ZA".unpack("C", offset: -1) assert_equal ["01000001"], "YZA".unpack("B*", offset: 2) assert_equal [nil, nil, nil], "abc".unpack("CCC", offset: 3) - assert_raise_with_message(ArgumentError, /offset can't be negative/) { - "a".unpack("C", offset: -1) - } assert_raise_with_message(ArgumentError, /offset outside of string/) { "a".unpack("C", offset: 2) } + assert_raise_with_message(ArgumentError, /offset outside of string/) { + "a".unpack("C", offset: -2) + } assert_equal [nil], "a".unpack("C", offset: 1) end @@ -936,4 +979,116 @@ EXPECTED assert_equal "oh no", v end; end + + def test_unpack_broken_R + assert_equal([nil], "\xFF".unpack("R")) + assert_nil("\xFF".unpack1("R")) + assert_equal([nil], "\xFF".unpack("r")) + assert_nil("\xFF".unpack1("r")) + + bytes = [256].pack("r") + assert_equal([256, nil, nil, nil], (bytes + "\xFF").unpack("rrrr")) + + bytes = [256].pack("R") + assert_equal([256, nil, nil, nil], (bytes + "\xFF").unpack("RRRR")) + + assert_equal([], "\xFF".unpack("R*")) + assert_equal([], "\xFF".unpack("r*")) + end + + def test_pack_unpack_R + # ULEB128 encoding (unsigned) + assert_equal("\x00", [0].pack("R")) + assert_equal("\x01", [1].pack("R")) + assert_equal("\x7f", [127].pack("R")) + assert_equal("\x80\x01", [128].pack("R")) + assert_equal("\xff\x7f", [0x3fff].pack("R")) + assert_equal("\x80\x80\x01", [0x4000].pack("R")) + assert_equal("\xff\xff\xff\xff\x0f", [0xffffffff].pack("R")) + assert_equal("\x80\x80\x80\x80\x10", [0x100000000].pack("R")) + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", [0xffff_ffff_ffff_ffff].pack("R")) + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("R")) + + # Multiple values + assert_equal("\x01\x02", [1, 2].pack("R*")) + assert_equal("\x7f\x80\x01", [127, 128].pack("R*")) + + # Negative numbers should raise an error + assert_raise(ArgumentError) { [-1].pack("R") } + assert_raise(ArgumentError) { [-100].pack("R") } + + # Unpack tests + assert_equal([0], "\x00".unpack("R")) + assert_equal([1], "\x01".unpack("R")) + assert_equal([127], "\x7f".unpack("R")) + assert_equal([128], "\x80\x01".unpack("R")) + assert_equal([0x3fff], "\xff\x7f".unpack("R")) + assert_equal([0x4000], "\x80\x80\x01".unpack("R")) + assert_equal([0xffffffff], "\xff\xff\xff\xff\x0f".unpack("R")) + assert_equal([0x100000000], "\x80\x80\x80\x80\x10".unpack("R")) + assert_equal([0xffff_ffff_ffff_ffff], "\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01".unpack("R")) + assert_equal([0xffff_ffff_ffff_ffff_ffff_ffff].pack("R"), "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f") + + # Multiple values + assert_equal([1, 2], "\x01\x02".unpack("R*")) + assert_equal([127, 128], "\x7f\x80\x01".unpack("R*")) + + # Round-trip test + values = [0, 1, 127, 128, 0x3fff, 0x4000, 0xffffffff, 0x100000000] + assert_equal(values, values.pack("R*").unpack("R*")) + end + + def test_pack_unpack_r + # SLEB128 encoding (signed) + assert_equal("\x00", [0].pack("r")) + assert_equal("\x01", [1].pack("r")) + assert_equal("\x7f", [-1].pack("r")) + assert_equal("\x7e", [-2].pack("r")) + assert_equal("\xff\x00", [127].pack("r")) + assert_equal("\x80\x01", [128].pack("r")) + assert_equal("\x81\x7f", [-127].pack("r")) + assert_equal("\x80\x7f", [-128].pack("r")) + + # Larger positive numbers + assert_equal("\xff\xff\x00", [0x3fff].pack("r")) + assert_equal("\x80\x80\x01", [0x4000].pack("r")) + + # Larger negative numbers + assert_equal("\x81\x80\x7f", [-0x3fff].pack("r")) + assert_equal("\x80\x80\x7f", [-0x4000].pack("r")) + + # Very large numbers + assert_equal("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1F", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + assert_equal("\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80`", [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + + # Multiple values + assert_equal("\x00\x01\x7f", [0, 1, -1].pack("r*")) + + # Unpack tests + assert_equal([0], "\x00".unpack("r")) + assert_equal([1], "\x01".unpack("r")) + assert_equal([-1], "\x7f".unpack("r")) + assert_equal([-2], "\x7e".unpack("r")) + assert_equal([127], "\xff\x00".unpack("r")) + assert_equal([128], "\x80\x01".unpack("r")) + assert_equal([-127], "\x81\x7f".unpack("r")) + assert_equal([-128], "\x80\x7f".unpack("r")) + + # Larger numbers + assert_equal([0x3fff], "\xff\xff\x00".unpack("r")) + assert_equal([0x4000], "\x80\x80\x01".unpack("r")) + assert_equal([-0x3fff], "\x81\x80\x7f".unpack("r")) + assert_equal([-0x4000], "\x80\x80\x7f".unpack("r")) + + # Very large numbers + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + assert_equal("\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80`", [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + + # Multiple values + assert_equal([0, 1, -1], "\x00\x01\x7f".unpack("r*")) + + # Round-trip test + values = [0, 1, -1, 127, -127, 128, -128, 0x3fff, -0x3fff, 0x4000, -0x4000] + assert_equal(values, values.pack("r*").unpack("r*")) + end end diff --git a/test/ruby/test_parse.rb b/test/ruby/test_parse.rb index 98e95b98af..def41d6017 100644 --- a/test/ruby/test_parse.rb +++ b/test/ruby/test_parse.rb @@ -352,6 +352,21 @@ class TestParse < Test::Unit::TestCase assert_equal("foobar", b) end + def test_call_command + a = b = nil + o = Object.new + def o.m(*arg); proc {|a| arg.join + a }; end + + assert_nothing_raised do + o.instance_eval <<-END, __FILE__, __LINE__+1 + a = o.m "foo", "bar" do end.("buz") + b = o.m "foo", "bar" do end::("buz") + END + end + assert_equal("foobarbuz", a) + assert_equal("foobarbuz", b) + end + def test_xstring assert_raise(Errno::ENOENT) do eval("``") @@ -1544,7 +1559,7 @@ x = __ENCODING__ end def test_shareable_constant_value_simple - obj = [['unsharable_value']] + obj = [['unshareable_value']] a, b, c = eval_separately("#{<<~"begin;"}\n#{<<~'end;'}") begin; # shareable_constant_value: experimental_everything @@ -1573,7 +1588,7 @@ x = __ENCODING__ assert_ractor_shareable(a) assert_not_ractor_shareable(obj) assert_equal obj, a - assert !obj.equal?(a) + assert_not_same obj, a bug_20339 = '[ruby-core:117186] [Bug #20339]' bug_20341 = '[ruby-core:117197] [Bug #20341]' diff --git a/test/ruby/test_pattern_matching.rb b/test/ruby/test_pattern_matching.rb index 92a3244fc2..96aa2a7fd6 100644 --- a/test/ruby/test_pattern_matching.rb +++ b/test/ruby/test_pattern_matching.rb @@ -197,11 +197,49 @@ class TestPatternMatching < Test::Unit::TestCase end end - assert_syntax_error(%q{ + assert_valid_syntax(%{ + case 0 + in [ :a | :b, x] + true + end + }) + + assert_in_out_err(['-c'], %q{ case 0 in a | 0 end - }, /illegal variable in alternative pattern/) + }, [], /alternative pattern/, + success: false) + + assert_in_out_err(['-c'], %q{ + case 0 + in 0 | a + end + }, [], /alternative pattern/, + success: false) + end + + def test_alternative_pattern_nested + assert_in_out_err(['-c'], %q{ + case 0 + in [a] | 1 + end + }, [], /alternative pattern/, + success: false) + + assert_in_out_err(['-c'], %q{ + case 0 + in { a: b } | 1 + end + }, [], /alternative pattern/, + success: false) + + assert_in_out_err(['-c'], %q{ + case 0 + in [{ a: [{ b: [{ c: }] }] }] | 1 + end + }, [], /alternative pattern/, + success: false) end def test_var_pattern diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index 2cd97ca324..f74342322f 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -513,7 +513,7 @@ class TestProc < Test::Unit::TestCase file, lineno = method(:source_location_test).to_proc.binding.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test[0], lineno, 'Bug #2427') + assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') end def test_binding_error_unless_ruby_frame @@ -1499,19 +1499,15 @@ class TestProc < Test::Unit::TestCase assert_include(EnvUtil.labeled_class(name, Proc).new {}.to_s, name) end - @@line_of_source_location_test = [__LINE__ + 1, 2, __LINE__ + 3, 5] + @@line_of_source_location_test = __LINE__ + 1 def source_location_test a=1, b=2 end def test_source_location - file, *loc = method(:source_location_test).source_location + file, lineno = method(:source_location_test).source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') - - file, *loc = self.class.instance_method(:source_location_test).source_location - assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') + assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') end @@line_of_attr_reader_source_location_test = __LINE__ + 3 @@ -1544,13 +1540,13 @@ class TestProc < Test::Unit::TestCase end def test_block_source_location - exp_loc = [__LINE__ + 3, 49, __LINE__ + 4, 49] - file, *loc = block_source_location_test(1, + exp_lineno = __LINE__ + 3 + file, lineno = block_source_location_test(1, 2, 3) do end assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp_loc, loc) + assert_equal(exp_lineno, lineno) end def test_splat_without_respond_to @@ -1659,29 +1655,99 @@ class TestProc < Test::Unit::TestCase assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:_9) } assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } "bar".tap do assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:_9) } assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } end assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:_9) } assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } end "foo".tap do assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:_9) } assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } "bar".tap do _9 and flunk assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:_9) } assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } end assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:_9) } assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } + end + end + + def test_implicit_parameters_for_numparams + x = x = 1 + assert_raise(NameError) { binding.implicit_parameter_get(:x) } + assert_raise(NameError) { binding.implicit_parameter_defined?(:x) } + + "foo".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + "bar".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + end + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + end + + "foo".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + "bar".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("bar", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + end + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) end end @@ -1690,32 +1756,165 @@ class TestProc < Test::Unit::TestCase it assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) "bar".tap do assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) end assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) "bar".tap do it assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) end assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) end "foo".tap do assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) "bar".tap do it assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) end assert_equal([], binding.local_variables) assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) + end + end + + def test_implicit_parameters_for_it + "foo".tap do + it or flunk + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + "bar".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + + "foo".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + "bar".tap do + it or flunk + assert_equal([:it], binding.implicit_parameters) + assert_equal("bar", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + end + + def test_implicit_parameters_for_it_complex + "foo".tap do + it = it = "bar" + + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + + assert_equal([:it], binding.local_variables) + assert_equal("bar", binding.local_variable_get(:it)) + assert_equal(true, binding.local_variable_defined?(:it)) + end + + "foo".tap do + it or flunk + + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_equal(true, binding.implicit_parameter_defined?(:it)) + + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) + end + + "foo".tap do + it or flunk + it = it = "bar" + + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_equal(true, binding.implicit_parameter_defined?(:it)) + + assert_equal([:it], binding.local_variables) + assert_equal("bar", binding.local_variable_get(:it)) + assert_equal(true, binding.local_variable_defined?(:it)) + end + end + + def test_implicit_parameters_for_it_and_numparams + "foo".tap do + it or flunk + "bar".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal("bar", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end end + + "foo".tap do + _5 and flunk + "bar".tap do + it or flunk + assert_equal([:it], binding.implicit_parameters) + assert_equal("bar", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_5) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end + end + end + + def test_implicit_parameter_invalid_name + message_pattern = /is not an implicit parameter/ + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_defined?(:foo) } + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_get(:foo) } + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_defined?("wrong_implicit_parameter_name_#{rand(10000)}") } + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_get("wrong_implicit_parameter_name_#{rand(10000)}") } end def test_local_variable_set_wb diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb index 221ff37c6b..d99e356e69 100644 --- a/test/ruby/test_process.rb +++ b/test/ruby/test_process.rb @@ -58,6 +58,8 @@ class TestProcess < Test::Unit::TestCase def test_rlimit_nofile return unless rlimit_exist? + omit "LSAN needs to open proc file" if Test::Sanitizers.lsan_enabled? + with_tmpchdir { File.write 's', <<-"End" # Too small RLIMIT_NOFILE, such as zero, causes problems. @@ -280,21 +282,22 @@ class TestProcess < Test::Unit::TestCase end; end - MANDATORY_ENVS = %w[RUBYLIB GEM_HOME GEM_PATH] - case RbConfig::CONFIG['target_os'] - when /linux/ - MANDATORY_ENVS << 'LD_PRELOAD' - when /mswin|mingw/ - MANDATORY_ENVS.concat(%w[HOME USER TMPDIR PROCESSOR_ARCHITECTURE]) - when /darwin/ - MANDATORY_ENVS.concat(ENV.keys.grep(/\A__CF_/)) - end + MANDATORY_ENVS = %w[RUBYLIB GEM_HOME GEM_PATH RUBY_FREE_AT_EXIT] if e = RbConfig::CONFIG['LIBPATHENV'] MANDATORY_ENVS << e end if e = RbConfig::CONFIG['PRELOADENV'] and !e.empty? MANDATORY_ENVS << e end + case RbConfig::CONFIG['target_os'] + when /mswin|mingw/ + MANDATORY_ENVS.concat(%w[HOME USER TMPDIR PROCESSOR_ARCHITECTURE]) + when /darwin/ + MANDATORY_ENVS.concat(%w[TMPDIR], ENV.keys.grep(/\A__CF_/)) + # IO.popen([ENV.keys.to_h {|e| [e, nil]}, + # RUBY, "-e", %q[print ENV.keys.join(?\0)]], + # &:read).split(?\0) + end PREENVARG = ['-e', "%w[#{MANDATORY_ENVS.join(' ')}].each{|e|ENV.delete(e)}"] ENVARG = ['-e', 'ENV.each {|k,v| puts "#{k}=#{v}" }'] ENVCOMMAND = [RUBY].concat(PREENVARG).concat(ENVARG) @@ -1565,7 +1568,7 @@ class TestProcess < Test::Unit::TestCase def test_wait_exception bug11340 = '[ruby-dev:49176] [Bug #11340]' t0 = t1 = nil - sec = 3 + sec = EnvUtil.apply_timeout_scale(3) code = "puts;STDOUT.flush;Thread.start{gets;exit};sleep(#{sec})" IO.popen([RUBY, '-e', code], 'r+') do |f| pid = f.pid @@ -1993,7 +1996,7 @@ class TestProcess < Test::Unit::TestCase end def test_popen_reopen - assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; io = File.open(IO::NULL) io2 = io.dup @@ -2384,7 +2387,7 @@ EOS end def test_deadlock_by_signal_at_forking - assert_separately(%W(- #{RUBY}), <<-INPUT, timeout: 100) + assert_ruby_status(%W(- #{RUBY}), <<-INPUT, timeout: 100) ruby = ARGV.shift GC.start # reduce garbage GC.disable # avoid triggering CoW after forks @@ -2771,12 +2774,12 @@ EOS # Disable GC so we can make sure GC only runs in Process.warmup GC.disable - total_slots_before = GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_slots) + total_slots_before = GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_bytes) / GC.stat_heap(0, :slot_size) Process.warmup # TODO: flaky - # assert_equal(total_slots_before, GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_slots)) + # assert_equal(total_slots_before, GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_bytes) / GC.stat_heap(0, :slot_size)) assert_equal(0, GC.stat(:heap_empty_pages)) assert_operator(GC.stat(:total_freed_pages), :>, 0) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index c1f33798ba..e7eb0cd4b3 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -3,38 +3,19 @@ require 'test/unit' class TestRactor < Test::Unit::TestCase def test_shareability_of_iseq_proc - y = nil.instance_eval do + assert_raise Ractor::IsolationError do foo = [] - proc { foo } + Ractor.shareable_proc{ foo } end - assert_unshareable(y, /unshareable object \[\] from variable 'foo'/) - - y = [].instance_eval { proc { self } } - assert_unshareable(y, /Proc's self is not shareable/) - - y = [].freeze.instance_eval { proc { self } } - assert_make_shareable(y) - end - - def test_shareability_of_curried_proc - x = nil.instance_eval do - foo = [] - proc { foo }.curry - end - assert_unshareable(x, /unshareable object \[\] from variable 'foo'/) - - x = nil.instance_eval do - foo = 123 - proc { foo }.curry - end - assert_make_shareable(x) end def test_shareability_of_method_proc + # TODO: fix with Ractor.shareable_proc/lambda +=begin str = +"" x = str.instance_exec { proc { to_s } } - assert_unshareable(x, /Proc's self is not shareable/) + assert_unshareable(x, /Proc\'s self is not shareable/) x = str.instance_exec { method(:to_s) } assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()>", exception: Ractor::Error) @@ -58,6 +39,60 @@ class TestRactor < Test::Unit::TestCase x = str.instance_exec { method(:itself).to_proc } assert_unshareable(x, "can not make shareable object for #<Method: String(Kernel)#itself()>", exception: Ractor::Error) +=end + end + + def test_shareable_proc_define_method_super_method_missing + assert_ractor(<<~'RUBY', timeout: 30) + iterations = 1_000_000 + + class SuperFromShareableProcMethodMissingBase + def method_missing(mid, *) = mid + end + + class SuperFromShareableProcMethodMissingChild < SuperFromShareableProcMethodMissingBase + BODY = Ractor.shareable_proc { super() } + define_method(:foo, &BODY) + define_method(:bar, &BODY) + end + + [:foo, :bar].map do |mid| + Ractor.new(mid, iterations) do |mid, iterations| + obj = SuperFromShareableProcMethodMissingChild.new + iterations.times do + got = obj.__send__(mid) + raise "#{mid} returned #{got.inspect}" unless got == mid + end + end + end.each(&:value) + RUBY + end + + def test_shareable_proc_define_method_super_method_entry + assert_ractor(<<~'RUBY', timeout: 30) + iterations = 1_000_000 + + class SuperFromShareableProcBase + def foo = :foo + def bar = :bar + end + + class SuperFromShareableProcChild < SuperFromShareableProcBase + BODY = Ractor.shareable_proc { super() } + define_method(:foo, &BODY) + define_method(:bar, &BODY) + end + + [:foo, :bar].map do |mid| + Ractor.new(mid, iterations) do |mid, iterations| + obj = SuperFromShareableProcChild.new + iterations.times do + got = obj.__send__(mid) + raise "#{mid} returned #{got.inspect}" unless got == mid + end + end + end.each(&:value) + RUBY end def test_shareability_error_uses_inspect @@ -65,7 +100,58 @@ class TestRactor < Test::Unit::TestCase def x.to_s raise "this should not be called" end - assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()>", exception: Ractor::Error) + assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()> because it refers unshareable objects", exception: Ractor::Error) + end + + def test_sending_exception_with_backtrace + assert_ractor(<<~'RUBY') + def build_error + raise "Test" + rescue => error + error + end + + error = build_error + refute_empty error.backtrace + refute_empty error.backtrace_locations + + backtrace, backtrace_locations = Ractor.new(error) do |error2| + [error2.backtrace, error2.backtrace_locations] + end.value + + assert_equal error.backtrace, backtrace + refute_empty backtrace_locations + RUBY + end + + def test_sending_exception_with_array_backtrace + assert_ractor(<<~'RUBY') + error = StandardError.new + error.set_backtrace(["foo", "bar"]) + refute_empty error.backtrace + assert_nil error.backtrace_locations + + backtrace, backtrace_locations = Ractor.new(error) do |error2| + [error2.backtrace, error2.backtrace_locations] + end.value + + assert_equal error.backtrace, backtrace + assert_nil backtrace_locations + RUBY + end + + def test_sending_object_with_broken_clone + assert_ractor(<<~'RUBY') + o = Object.new + def o.clone + self + end + ractor = Ractor.new { Ractor.receive } + error = assert_raise Ractor::Error do + ractor.send(o) + end + assert_match "#clone returned self", error.message + RUBY end def test_default_thread_group @@ -99,6 +185,22 @@ class TestRactor < Test::Unit::TestCase RUBY end + + def test_class_variables + # [Bug #22072] + assert_ractor(<<~'RUBY') + module Foo + def self.foo = @@foo + end + + Foo.class_variable_set(:@@foo, 1) + + 10.times { |i| Foo.class_variable_set(:"@@bar#{i}", i) } + + assert_equal(Foo.foo, 1) + RUBY + end + def test_struct_instance_variables assert_ractor(<<~'RUBY') StructIvar = Struct.new(:member) do @@ -117,6 +219,16 @@ class TestRactor < Test::Unit::TestCase RUBY end + def test_move_nested_hash_during_gc_with_yjit + assert_ractor(<<~'RUBY', timeout: 20, args: [{ "RUBY_YJIT_ENABLE" => "1" }]) + GC.stress = true + hash = { foo: { bar: "hello" }, baz: { qux: "there" } } + result = Ractor.new { Ractor.receive }.send(hash, move: true).value + assert_equal "hello", result[:foo][:bar] + assert_equal "there", result[:baz][:qux] + RUBY + end + def test_fork_raise_isolation_error assert_ractor(<<~'RUBY') ractor = Ractor.new do @@ -164,7 +276,7 @@ class TestRactor < Test::Unit::TestCase # [Bug #21398] def test_port_receive_dnt_with_port_send - omit 'unstable on windows and macos-14' if RUBY_PLATFORM =~ /mswin|darwin/ + omit 'unstable on windows and macos-14' if RUBY_PLATFORM =~ /mswin|mingw|darwin/ assert_ractor(<<~'RUBY', timeout: 90) THREADS = 10 JOBS_PER_THREAD = 50 @@ -209,6 +321,57 @@ class TestRactor < Test::Unit::TestCase RUBY end + def test_mn_threads + # Ideally, we would assert that vm->ractor.sched.max_cpu equals sysconf(_SC_NPROCESSORS_ONLN) + # when RUBY_MAX_CPU is not set. + assert_ractor(<<~'RUBY', args: [{ "RUBY_MN_THREADS" => "1" }]) + require "etc" + n = Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 8 + rs = n.times.map { Ractor.new { :ok } } + assert_equal [:ok] * n, rs.map(&:value) + RUBY + end + + def test_symbol_proc_is_shareable + pr = :symbol.to_proc + assert_make_shareable(pr) + end + + # [Bug #21775] + def test_ifunc_proc_not_shareable + h = Hash.new { self } + pr = h.to_proc + assert_unshareable(pr, /not supported yet/, exception: RuntimeError) + end + + def test_copy_unshareable_object_error_message + assert_ractor(<<~'RUBY') + pr = proc {} + err = assert_raise(Ractor::Error) do + Ractor.new(pr) {}.join + end + assert_match(/can not copy Proc object/, err.message) + RUBY + end + + def test_ractor_new_raises_isolation_error_if_outer_variables_are_accessed + assert_raise(Ractor::IsolationError) do + channel = Ractor::Port.new + Ractor.new(channel) do + inbound_work = Ractor::Port.new + channel << inbound_work + end + end + end + + def test_ractor_new_raises_isolation_error_if_proc_uses_yield + assert_raise(Ractor::IsolationError) do + Ractor.new do + yield + end + end + end + def assert_make_shareable(obj) refute Ractor.shareable?(obj), "object was already shareable" Ractor.make_shareable(obj) diff --git a/test/ruby/test_range.rb b/test/ruby/test_range.rb index f875c0ab40..ff17dca69e 100644 --- a/test/ruby/test_range.rb +++ b/test/ruby/test_range.rb @@ -36,6 +36,7 @@ class TestRange < Test::Unit::TestCase assert_equal(["a"], ("a" ... "b").to_a) assert_equal(["a", "b"], ("a" .. "b").to_a) assert_equal([*"a".."z", "aa"], ("a"..).take(27)) + assert_equal([*"a".."z"], eval("('a' || 'b')..'z'").to_a) end def test_range_numeric_string @@ -1457,6 +1458,12 @@ class TestRange < Test::Unit::TestCase assert_raise(RangeError) { (1..).to_a } end + def test_to_set + assert_equal(Set[1,2,3,4,5], (1..5).to_set) + assert_equal(Set[1,2,3,4], (1...5).to_set) + assert_raise(RangeError) { (1..).to_set } + end + def test_beginless_range_iteration assert_raise(TypeError) { (..1).each { } } end diff --git a/test/ruby/test_rational.rb b/test/ruby/test_rational.rb index e0edbde463..a02e11acc5 100644 --- a/test/ruby/test_rational.rb +++ b/test/ruby/test_rational.rb @@ -65,7 +65,7 @@ class Rational_Test < Test::Unit::TestCase assert_instance_of(String, c.to_s) end - def test_conv + def test_conv_integer c = Rational(0,1) assert_equal(Rational(0,1), c) @@ -94,6 +94,11 @@ class Rational_Test < Test::Unit::TestCase c = Rational(Rational(1,2),Rational(1,2)) assert_equal(Rational(1), c) + assert_equal(Rational(3),Rational(3)) + assert_equal(Rational(1),Rational(3,3)) + end + + def test_conv_complex c = Rational(Complex(1,2),2) assert_equal(Complex(Rational(1,2),1), c) @@ -102,11 +107,21 @@ class Rational_Test < Test::Unit::TestCase c = Rational(Complex(1,2),Complex(1,2)) assert_equal(Rational(1), c) + end - assert_equal(Rational(3),Rational(3)) - assert_equal(Rational(1),Rational(3,3)) + def test_conv_float assert_equal(3.3.to_r,Rational(3.3)) assert_equal(1,Rational(3.3,3.3)) + + if (0.0/0).nan? + assert_raise(FloatDomainError){Rational(0.0/0)} + end + if (1.0/0).infinite? + assert_raise(FloatDomainError){Rational(1.0/0)} + end + end + + def test_conv_string assert_equal(Rational(3),Rational('3')) assert_equal(Rational(1),Rational('3.0','3.0')) assert_equal(Rational(1),Rational('3/3','3/3')) @@ -115,6 +130,10 @@ class Rational_Test < Test::Unit::TestCase assert_equal(Rational(111, 10), Rational('1.11e1')) assert_equal(Rational(111, 100), Rational('1.11e0')) assert_equal(Rational(111, 1000), Rational('1.11e-1')) + assert_equal(Rational(5, 4), Rational('3.0r','2.4R')) + end + + def test_conv_error assert_raise(TypeError){Rational(nil)} assert_raise(ArgumentError){Rational('')} @@ -131,7 +150,9 @@ class Rational_Test < Test::Unit::TestCase assert_raise(TypeError){Rational(Object.new)} assert_raise(TypeError){Rational(Object.new, Object.new)} assert_raise(TypeError){Rational(1, Object.new)} + end + def test_conv_coerce bug12485 = '[ruby-core:75995] [Bug #12485]' o = Object.new def o.to_int; 1; end @@ -163,13 +184,6 @@ class Rational_Test < Test::Unit::TestCase assert_raise(ArgumentError){Rational()} assert_raise(ArgumentError){Rational(1,2,3)} - if (0.0/0).nan? - assert_raise(FloatDomainError){Rational(0.0/0)} - end - if (1.0/0).infinite? - assert_raise(FloatDomainError){Rational(1.0/0)} - end - bug16518 = "[ruby-core:96942] [Bug #16518]" cls = Class.new(Numeric) do def /(y); 42; end @@ -829,6 +843,10 @@ class Rational_Test < Test::Unit::TestCase ng[5, 3, '5/3x'] ng[5, 1, '5/-3'] + + ok[30, 24, '3.0r/2.4R'] + ng[30, 24, '3.0r/2.4re1'] + ng[30, 240, '3.0r/2.4e1r'] end def test_parse_zero_denominator diff --git a/test/ruby/test_refinement.rb b/test/ruby/test_refinement.rb index 6ce434790b..dce09c2dd8 100644 --- a/test/ruby/test_refinement.rb +++ b/test/ruby/test_refinement.rb @@ -1035,6 +1035,43 @@ class TestRefinement < Test::Unit::TestCase RUBY end + def test_prohibit_super_in_refined_module_method + assert_separately([], <<-"end;") + bug22071 = '[ruby-core:125511] [Bug #22071]' + class BasicObject + def a; "B" end + end + + module G + def a; "G" + super end + end + + module F + include G + def a; "F" + super end + end + + class A + def a; "A" + super end + end + + class B < A + include F + end + + module R + refine F do + def a; "R"+super end + end + end + using R + + msg = "super in a method in a module that has been refined and that is called via super" + + " from a refinement method is not supported." + assert_raise(NoMethodError, msg, bug22071) { B.new.a } + end; + end + def test_refine_after_using assert_separately([], <<-"end;") bug8880 = '[ruby-core:57079] [Bug #8880]' @@ -1058,6 +1095,613 @@ class TestRefinement < Test::Unit::TestCase end; end + { + zsuper: "public :a", + super: "def a = super" + }.each do |desc, method_def| + define_method :"test_modify_#{desc}_refinement_method_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + alias a a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + class A + def a = :b + end + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_modify_#{desc}_refinement_method_in_module_prepended_to_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :a + alias a a + end + + class A + prepend M + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_modify_#{desc}_refinement_method_in_module_included_in_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :a + alias a a + end + + class A + include M + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + private def a = :b + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + class B + remove_method(:a) + end + assert_equal(:a, C.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_prepended_to_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, B.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_prepended_to_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:b, A.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, A.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_included_in_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, C.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_included_in_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, B.new.a) + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + private def a = :b + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + class B + undef_method(:a) + end + assert_raise(NoMethodError) { C.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_prepended_to_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_prepended_to_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:b, A.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { A.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_included_in_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { C.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_included_in_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_to_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:a, A.new.a) + + module M + def a = :b + end + A.prepend M + assert_equal(:b, A.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_to_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + A.prepend M + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_in_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + B.include M + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:a, C.new.a) + + module M + def a = :b + end + B.include M + assert_equal(:b, C.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_undef_to_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:a, A.new.a) + + module M + def a = :b + undef_method :a + end + A.prepend M + assert_raise(NoMethodError) { A.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_undef_to_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + undef_method :a + end + A.prepend M + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_undef_in_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + undef_method :a + end + B.include M + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_undef_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:a, C.new.a) + + module M + def a = :b + undef_method :a + end + B.include M + assert_raise(NoMethodError) { C.new.a } + end; + end + end + + def test_zsuper_refinement_method_arity_and_parameters + assert_separately([], <<-"end;") + class A + private def a(b) = b + end + + class B < A + public :a + end + + module R + refine A do + public :a + end + end + using R + + m = B.instance_method(:a) + assert_equal(1, m.arity) + assert_equal([[:req, :b]], m.parameters) + + m = A.instance_method(:a) + assert_equal(1, m.arity) + assert_equal([[:req, :b]], m.parameters) + end; + end + def test_instance_methods bug8881 = '[ruby-core:57080] [Bug #8881]' assert_not_include(Foo.instance_methods(false), :z, bug8881) @@ -1933,6 +2577,29 @@ class TestRefinement < Test::Unit::TestCase end; end + def test_public_in_refine_for_method_in_superclass + assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}") + begin; + bug21446 = '[ruby-core:122558] [Bug #21446]' + + class CowSuper + private + def moo() "Moo"; end + end + class Cow < CowSuper + end + + module PublicCows + refine(Cow) { + public :moo + } + end + + using PublicCows + assert_equal("Moo", Cow.new.moo, bug21446) + end; + end + module SuperToModule class Parent end @@ -2232,7 +2899,7 @@ class TestRefinement < Test::Unit::TestCase def test_refining_module_repeatedly bug14070 = '[ruby-core:83617] [Bug #14070]' - assert_in_out_err([], <<-INPUT, ["ok"], [], bug14070) + assert_in_out_err([], <<-INPUT, ["ok"], [], bug14070, timeout: 30) 1000.times do Class.new do include Enumerable @@ -2712,6 +3379,287 @@ class TestRefinement < Test::Unit::TestCase INPUT end + def test_refined_module_method + m = Module.new { + x = Module.new {def qux;end} + refine(x) {def qux;end} + break x + } + extend m + meth = method(:qux) + assert_equal m, meth.owner + assert_equal :qux, meth.name + end + + def test_symbol_proc_from_using_scope + # assert_separately to contain the side effects of refining Kernel + assert_separately([], <<~RUBY) + class RefinedScope + using(Module.new { refine(Kernel) { def itself = 0 } }) + ITSELF = :itself.to_proc + end + + assert_equal(1, RefinedScope::ITSELF[1], "[Bug #21265]") + RUBY + end + + def test_method_super_method_single_refinements + assert_separately([], <<~RUBY) + class A + def b = "A" + end + module M + R = refine(A) { def b; "M" + super; end } + end + using M + a = A.new + m = a.method(:b) + assert_equal("MA", a.b) + assert_equal("MA", m.call) + assert_equal(M::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + + def test_method_super_method_multiple_refinements_with_activated_refinements_during_super + assert_separately([], <<~RUBY) + class A + def b = "A" + end + module M + R = refine(A) { def b; "M" + super; end } + end + module N + using M + R = refine(A) { def b; "N" + super; end } + end + using M + using N + a = A.new + m = a.method(:b) + assert_equal("NMA", a.b) + assert_equal("NMA", m.call) + assert_equal(N::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(M::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + + def test_method_super_method_multiple_refinements_without_activated_refinements_during_super + assert_separately([], <<~RUBY) + class A + def b = "A" + end + module M + R = refine(A) { def b; "M" + super; end } + end + module N + R = refine(A) { def b; "N" + super; end } + end + using M + using N + a = A.new + m = a.method(:b) + assert_equal("NA", a.b) + assert_equal("NA", m.call) + assert_equal(N::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + + def test_unbound_method_super_method_single_refinements + assert_separately([], <<~RUBY) + class A + def b = "A" + end + module M + R = refine(A) { def b; "M" + super; end } + end + using M + m = A.instance_method(:b) + assert_equal(M::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + + def test_method_super_method_nonrefined_finds_refined_super + assert_separately([], <<~RUBY) + class A + def b = "A" + end + module M + R = refine(A) { def b; "M" + super; end } + end + using M + class B < A + def b = "B" + super + end + b = B.new + m = b.method(:b) + assert_equal("BMA", b.b) + assert_equal("BMA", m.call) + assert_equal(B, m.owner) + m = m.super_method + assert_equal(M::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + + def test_method_super_method_refined_finds_refined_method_in_superclass + assert_separately([], <<~RUBY) + class A + def b = "A" + end + class B < A + end + module M + R = refine(A) { def b; "M" + super; end } + end + using M + module N + R = refine(B) { def b; "N" + super; end } + end + using N + + b = B.new + m = b.method(:b) + assert_equal("NMA", b.b) + assert_equal("NMA", m.call) + assert_equal(N::R, m.owner) + assert_equal(B, m.owner.target) + + m = m.super_method + assert_equal(M::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + + def test_method_super_method_uses_cref_of_method_not_cref_of_caller + assert_separately([], <<~RUBY) + class A + def b = "A" + end + class B < A + end + module M + R = refine(A) { def b; "M" + super; end } + end + module N + R = refine(B) do + using M + def b; "N" + super; end + end + end + using N + + b = B.new + m = b.method(:b) + assert_equal("NMA", b.b) + assert_equal("NMA", m.call) + assert_equal(N::R, m.owner) + assert_equal(B, m.owner.target) + + m = m.super_method + assert_equal(M::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + + def test_method_super_method_only_considers_activated_refinements + assert_separately([], <<~RUBY) + class A + def b = "A" + end + class B < A + def b = "B" + super + end + module M + R = refine(A){def b = "M" + super} + end + module N + R = refine(B){def b = "N" + super} + end + + module O + using M + using N + + b = B.new + m = b.method(:b) + assert_equal("NBA", b.b) + assert_equal("NBA", m.call) + assert_equal(N::R, m.owner) + assert_equal(B, m.owner.target) + + m = m.super_method + assert_equal(B, m.owner) + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + end + RUBY + end + + def test_method_super_method_bmethod_finds_refinements + assert_separately([], <<~RUBY) + class A + def b = "A" + end + module M + R = refine(A) { def b; "M" + super; end } + end + using M + class B < A + define_method(:b) { "B" + super() } + end + + b = B.new + m = b.method(:b) + assert_equal("BMA", b.b) + assert_equal("BMA", m.call) + assert_equal(B, m.owner) + + m = m.super_method + assert_equal(M::R, m.owner) + assert_equal(A, m.owner.target) + + m = m.super_method + assert_equal(A, m.owner) + assert_nil(m.super_method) + RUBY + end + private def eval_using(mod, s) diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb index 9f1e03e649..805c57b472 100644 --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -975,7 +975,7 @@ class TestRegexp < Test::Unit::TestCase def test_dup assert_equal(//, //.dup) - assert_raise(TypeError) { //.dup.instance_eval { initialize_copy(nil) } } + assert_raise(FrozenError) { //.dup.instance_eval { initialize_copy(/a/) } } end def test_regsub @@ -1011,6 +1011,18 @@ class TestRegexp < Test::Unit::TestCase end; end + def test_regsub_no_memory_leak_many_captures + assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~"end;"}", rss: true) + code = proc do + "aaaaaaaaaaa".gsub(/(a)(b)?(c)?(d)?(e)?(f)?(g)?(h)?/, "") + end + + 1_000.times(&code) + begin; + 100_000.times(&code) + end; + end + def test_ignorecase v = assert_deprecated_warning(/variable \$= is no longer effective/) { $= } assert_equal(false, v) @@ -1669,6 +1681,65 @@ class TestRegexp < Test::Unit::TestCase assert_equal("hoge fuga", h["body"]) end + def test_matchdata_large_capture_groups_stack + env = {"RUBY_THREAD_MACHINE_STACK_SIZE" => (256 * 1024).to_s} + assert_separately([env], <<~'RUBY') + n = 20000 + require "rbconfig/sizeof" + stack = RubyVM::DEFAULT_PARAMS[:thread_machine_stack_size] + size = RbConfig::SIZEOF["long"] + required = (n + 1) * 4 * size + if !stack || stack == 0 || stack >= required + omit "thread machine stack size not reduced (#{stack}:#{required})" + end + + inspect = Thread.new do + str = "\u{3042}" * n + m = Regexp.new("(.)" * n).match(str) + assert_not_nil(m) + assert_equal([n - 1, n], m.offset(n)) + m.inspect + end.value + + assert_include(inspect, "MatchData") + RUBY + end + + def test_match_integer_at + m = /(\d{4})(\d{2})(\d{2})/.match("20260308") + assert_equal(20260308, m.integer_at(0)) + assert_equal(2026, m.integer_at(1)) + assert_equal(3, m.integer_at(2)) + assert_equal(8, m.integer_at(3)) + assert_equal(nil, m.integer_at(4)) + assert_equal(8, m.integer_at(-1)) + assert_equal(3, m.integer_at(-2)) + assert_equal(2026, m.integer_at(-3)) + assert_equal(nil, m.integer_at(-4)) + + re = /[a-z]+|(\d+)/ + assert_equal(123, re.match("123").integer_at(1)) + assert_equal(nil, re.match("abc").integer_at(1)) + end + + def test_match_integer_at_name + m = /(?<y>\d{4})(?<m>\d{2})(?<d>\d{2})/.match("20260308") + assert_equal(2026, m.integer_at("y")) + assert_equal(3, m.integer_at("m")) + assert_equal(8, m.integer_at("d")) + end + + def test_match_integer_at_base + assert_equal(91, /\w+/.match("111").integer_at(0, 9)) + assert_equal(10_0000, /\w+/.match("10_0000").integer_at(0)) + assert_equal(0d1_0000, /\w+/.match("01_0000").integer_at(0)) + assert_equal(0o1_0000, /\w+/.match("01_0000").integer_at(0, 0)) + assert_equal(0b1_0000, /\w+/.match("0b1_0000").integer_at(0, 0)) + assert_equal(0o1_0000, /\w+/.match("0o1_0000").integer_at(0, 0)) + assert_equal(0d1_0000, /\w+/.match("0d1_0000").integer_at(0, 0)) + assert_equal(0x1_0000, /\w+/.match("0x1_0000").integer_at(0, 0)) + end + def test_regexp_popped EnvUtil.suppress_warning do assert_nothing_raised { eval("a = 1; /\#{ a }/; a") } @@ -1743,6 +1814,33 @@ class TestRegexp < Test::Unit::TestCase assert_raise(RegexpError, bug12418){ Regexp.new('(0?0|(?(5)||)|(?(5)||))?') } end + def test_quick_search + assert_match_at('(?i) *TOOKY', 'Mozilla/5.0 (Linux; Android 4.0.3; TOOKY', [[34, 40]]) # Issue #120 + end + + def test_ss_in_look_behind + assert_match_at("(?i:ss)", "ss", [[0, 2]]) + assert_match_at("(?i:ss)", "Ss", [[0, 2]]) + assert_match_at("(?i:ss)", "SS", [[0, 2]]) + assert_match_at("(?i:ss)", "\u017fS", [[0, 2]]) # LATIN SMALL LETTER LONG S + assert_match_at("(?i:ss)", "s\u017f", [[0, 2]]) + assert_match_at("(?i:ss)", "\u00df", [[0, 1]]) # LATIN SMALL LETTER SHARP S + assert_match_at("(?i:ss)", "\u1e9e", [[0, 1]]) # LATIN CAPITAL LETTER SHARP S + assert_match_at("(?i:xssy)", "xssy", [[0, 4]]) + assert_match_at("(?i:xssy)", "xSsy", [[0, 4]]) + assert_match_at("(?i:xssy)", "xSSy", [[0, 4]]) + assert_match_at("(?i:xssy)", "x\u017fSy", [[0, 4]]) + assert_match_at("(?i:xssy)", "xs\u017fy", [[0, 4]]) + assert_match_at("(?i:xssy)", "x\u00dfy", [[0, 3]]) + assert_match_at("(?i:xssy)", "x\u1e9ey", [[0, 3]]) + assert_match_at("(?i:\u00df)", "ss", [[0, 2]]) + assert_match_at("(?i:\u00df)", "SS", [[0, 2]]) + assert_match_at("(?i:[\u00df])", "ss", [[0, 2]]) + assert_match_at("(?i:[\u00df])", "SS", [[0, 2]]) + assert_match_at("(?i)(?<!ss)\u2728", "qq\u2728", [[2, 3]]) # Issue #92 + assert_match_at("(?i)(?<!xss)\u2728", "qq\u2728", [[2, 3]]) + end + def test_options_in_look_behind assert_nothing_raised { assert_match_at("(?<=(?i)ab)cd", "ABcd", [[2,4]]) @@ -1949,6 +2047,7 @@ class TestRegexp < Test::Unit::TestCase Regexp.timeout = 1e300 assert_equal(((1<<64)-1) / 1000000000.0, Regexp.timeout) + assert_raise(ArgumentError) { Regexp.timeout = Float::NAN } assert_raise(ArgumentError) { Regexp.timeout = 0 } assert_raise(ArgumentError) { Regexp.timeout = -1 } @@ -2041,6 +2140,7 @@ class TestRegexp < Test::Unit::TestCase assert_equal(((1<<64)-1) / 1000000000.0, Regexp.new("foo", timeout: 1e300).timeout) + assert_raise(ArgumentError) { Regexp.new("foo", timeout: Float::NAN) } assert_raise(ArgumentError) { Regexp.new("foo", timeout: 0) } assert_raise(ArgumentError) { Regexp.new("foo", timeout: -1) } end; @@ -2264,4 +2364,16 @@ class TestRegexp < Test::Unit::TestCase assert_match(/[x#{e_acute_lower}]/i, "CAF#{e_acute_upper}", "should match e acute case insensitive") end end + + def test_too_many_range_repeat + source = '(?:foobar){0,100}' * 100000 + assert_raise(RegexpError) { Regexp.new(source) } + assert_raise(SyntaxError) { eval("/#{source}/") } + end + + def test_too_many_null_check + source = '(?:(?:foo)?|(?:bar)?)*' * 100000 + assert_raise(RegexpError) { Regexp.new(source) } + assert_raise(SyntaxError) { eval("/#{source}/") } + end end diff --git a/test/ruby/test_require.rb b/test/ruby/test_require.rb index 13e7076391..eed8e97da8 100644 --- a/test/ruby/test_require.rb +++ b/test/ruby/test_require.rb @@ -54,7 +54,7 @@ class TestRequire < Test::Unit::TestCase end; begin - assert_in_out_err(["-S", "-w", "foo/" * 1024 + "foo"], "") do |r, e| + assert_in_out_err(["-S", "-w", (["foo"] * 1025).join("_")], "") do |r, e| assert_equal([], r) assert_operator(2, :<=, e.size) assert_match(/warning: openpath: pathname too long \(ignored\)/, e.first) @@ -840,6 +840,36 @@ class TestRequire < Test::Unit::TestCase p :ok end; } + + # [Bug #21567] + assert_ruby_status(%w[-rtempfile], "#{<<~"begin;"}\n#{<<~"end;"}") + begin; + class MyString + def initialize(path) + @path = path + end + + def to_str + $LOADED_FEATURES.clear + @path + end + + def to_path = @path + end + + FILES = [] + + def create_ruby_file + file = Tempfile.open(["test", ".rb"]) + FILES << file + file.path + end + + require MyString.new(create_ruby_file) + $LOADED_FEATURES.unshift(create_ruby_file) + $LOADED_FEATURES << MyString.new(create_ruby_file) + require create_ruby_file + end; end def test_loading_fifo_threading_raise @@ -999,7 +1029,7 @@ class TestRequire < Test::Unit::TestCase def test_require_with_public_method_missing # [Bug #19793] - assert_separately(["-W0", "-rtempfile"], __FILE__, __LINE__, <<~RUBY, timeout: 60) + assert_ruby_status(["-W0", "-rtempfile"], <<~RUBY, timeout: 60) GC.stress = true class Object @@ -1011,4 +1041,18 @@ class TestRequire < Test::Unit::TestCase end RUBY end + + def test_bug_21568 + load_path = $LOAD_PATH.dup + loaded_featrures = $LOADED_FEATURES.dup + + $LOAD_PATH.clear + $LOADED_FEATURES.replace(["foo.so", "a/foo.rb", "b/foo.rb"]) + + assert_nothing_raised(LoadError) { require "foo" } + + ensure + $LOAD_PATH.replace(load_path) if load_path + $LOADED_FEATURES.replace loaded_featrures + end end diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index b6c76ac73a..4a31f91b4a 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -8,8 +8,6 @@ require_relative '../lib/jit_support' require_relative '../lib/parser_support' class TestRubyOptions < Test::Unit::TestCase - def self.yjit_enabled? = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? - # Here we're defining our own RUBY_DESCRIPTION without "+PRISM". We do this # here so that the various tests that reference RUBY_DESCRIPTION don't have to # worry about it. The flag itself is tested in its own test. @@ -22,8 +20,10 @@ class TestRubyOptions < Test::Unit::TestCase NO_JIT_DESCRIPTION = case - when yjit_enabled? - RUBY_DESCRIPTION.sub(/\+YJIT( (dev|dev_nodebug|stats))? /, '') + when JITSupport.yjit_enabled? + RUBY_DESCRIPTION.sub(/\+YJIT( \w+)? /, '') + when JITSupport.zjit_enabled? + RUBY_DESCRIPTION.sub(/\+ZJIT( \w+)? /, '') else RUBY_DESCRIPTION end @@ -47,15 +47,15 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err([], "", [], []) end - version = RUBY_PATCHLEVEL == -1 ? "master" : "#{RUBY_VERSION_MAJOR}.#{RUBY_VERSION_MINOR}" - OPTIONS_LINK = "https://docs.ruby-lang.org/en/#{version}/ruby/options_md.html" + # This constant enforces the traditional 80x25 terminal size standard + TRADITIONAL_TERM_COLS = 80 # DO NOT MODIFY! + TRADITIONAL_TERM_ROWS = 25 # DO NOT MODIFY! def test_usage + # This test checks if the output of `ruby -h` fits in 80x25 assert_in_out_err(%w(-h)) do |r, e| - _, _, link, *r = r - assert_include(link, OPTIONS_LINK) - assert_operator(r.size, :<=, 24) - longer = r.select {|x| x.size >= 80} + assert_operator(r.size, :<=, TRADITIONAL_TERM_ROWS) + longer = r[1..-1].select {|x| x.size >= TRADITIONAL_TERM_COLS} assert_equal([], longer) assert_equal([], e) end @@ -63,9 +63,7 @@ class TestRubyOptions < Test::Unit::TestCase def test_usage_long assert_in_out_err(%w(--help)) do |r, e| - _, _, link, *r = r - assert_include(link, OPTIONS_LINK) - longer = r.select {|x| x.size > 80} + longer = r[1..-1].select {|x| x.size > 80} assert_equal([], longer) assert_equal([], e) end @@ -181,7 +179,7 @@ class TestRubyOptions < Test::Unit::TestCase def test_verbose assert_in_out_err([{'RUBY_YJIT_ENABLE' => nil}, "-vve", ""]) do |r, e| assert_match(VERSION_PATTERN, r[0]) - if self.class.yjit_enabled? && !JITSupport.yjit_force_enabled? + if (JITSupport.yjit_enabled? && !JITSupport.yjit_force_enabled?) || JITSupport.zjit_enabled? assert_equal(NO_JIT_DESCRIPTION, r[0]) else assert_equal(RUBY_DESCRIPTION, r[0]) @@ -210,6 +208,8 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err(%w(--enable foobarbazqux -e) + [""], "", [], /unknown argument for --enable: 'foobarbazqux'/) assert_in_out_err(%w(--enable), "", [], /missing argument for --enable/) + assert_in_out_err(%w(-e) + ['p defined? Gem'], "", %w["constant"], [], gems: true) + assert_in_out_err(%w(-e) + ['p defined? Gem'], "", %w["constant"], [], gems: nil) end def test_disable @@ -219,7 +219,7 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err(%w(--disable foobarbazqux -e) + [""], "", [], /unknown argument for --disable: 'foobarbazqux'/) assert_in_out_err(%w(--disable), "", [], /missing argument for --disable/) - assert_in_out_err(%w(-e) + ['p defined? Gem'], "", ["nil"], []) + assert_in_out_err(%w(-e) + ['p defined? Gem'], "", ["nil"], [], gems: false) assert_in_out_err(%w(--disable-did_you_mean -e) + ['p defined? DidYouMean'], "", ["nil"], []) assert_in_out_err(%w(-e) + ['p defined? DidYouMean'], "", ["nil"], []) end @@ -247,7 +247,7 @@ class TestRubyOptions < Test::Unit::TestCase assert_match(VERSION_PATTERN, r[0]) if ENV['RUBY_YJIT_ENABLE'] == '1' assert_equal(NO_JIT_DESCRIPTION, r[0]) - elsif self.class.yjit_enabled? # checking -DYJIT_FORCE_ENABLE + elsif JITSupport.yjit_enabled? || JITSupport.zjit_enabled? # checking -DYJIT_FORCE_ENABLE assert_equal(EnvUtil.invoke_ruby(['-e', 'print RUBY_DESCRIPTION'], '', true).first, r[0]) else assert_equal(RUBY_DESCRIPTION, r[0]) @@ -446,37 +446,28 @@ class TestRubyOptions < Test::Unit::TestCase def test_search rubypath_orig = ENV['RUBYPATH'] path_orig = ENV['PATH'] + libpath = (path_orig if path_orig && RbConfig::CONFIG['LIBPATHENV'] == 'PATH') - Tempfile.create(["test_ruby_test_rubyoption", ".rb"]) {|t| - t.puts "p 1" - t.close - - @verbose = $VERBOSE - $VERBOSE = nil - - path, name = File.split(t.path) + Dir.mktmpdir("test_ruby_test_rubyoption") do |path| + name = "test_rubyoption.rb" + parent, dir = File.split(path) + File.write("#{path}/#{name}", "p 1") + load_error = %r[#{Regexp.quote dir}/#{Regexp.quote name} \(LoadError\)] - ENV['PATH'] = (path_orig && RbConfig::CONFIG['LIBPATHENV'] == 'PATH') ? - [path, path_orig].join(File::PATH_SEPARATOR) : path + ENV['PATH'] = [path, *libpath].join(File::PATH_SEPARATOR) assert_in_out_err(%w(-S) + [name], "", %w(1), []) + ENV['PATH'] = [parent, *libpath].join(File::PATH_SEPARATOR) + assert_in_out_err(%W(-S) + ["#{dir}/#{name}"], "", [], load_error) ENV['PATH'] = path_orig ENV['RUBYPATH'] = path assert_in_out_err(%w(-S) + [name], "", %w(1), []) - } - - ensure - if rubypath_orig + ENV['RUBYPATH'] = parent + assert_in_out_err(%w(-S) + ["#{dir}/#{name}"], "", [], load_error) + ensure ENV['RUBYPATH'] = rubypath_orig - else - ENV.delete('RUBYPATH') - end - if path_orig ENV['PATH'] = path_orig - else - ENV.delete('PATH') end - $VERBOSE = @verbose end def test_shebang @@ -528,6 +519,8 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err(%w(- -#=foo), "#!ruby -s\n", [], /invalid name for global variable - -# \(NameError\)/) + + assert_in_out_err(['-s', '-e', 'GC.start; p $DEBUG', '--', '-DEBUG=x'], "", ['"x"']) end def test_option_missing_argument @@ -852,11 +845,13 @@ class TestRubyOptions < Test::Unit::TestCase # We want YJIT to be enabled in the subprocess if it's enabled for us # so that the Ruby description matches. env = Hash === args.first ? args.shift : {} - args.unshift("--yjit") if self.class.yjit_enabled? + args.unshift("--yjit") if JITSupport.yjit_enabled? + args.unshift("--zjit") if JITSupport.zjit_enabled? env.update({'RUBY_ON_BUG' => nil}) + env['RUBY_CRASH_REPORT'] ||= nil # default to not passing down parent setting # ASAN registers a segv handler which prints out "AddressSanitizer: DEADLYSIGNAL" when # catching sigsegv; we don't expect that output, so suppress it. - env.update({'ASAN_OPTIONS' => 'handle_segv=0'}) + env.update({'ASAN_OPTIONS' => 'handle_segv=0', 'LSAN_OPTIONS' => 'handle_segv=0'}) args.unshift(env) test_stdin = "" @@ -948,6 +943,27 @@ class TestRubyOptions < Test::Unit::TestCase end end + def test_crash_report_pipe_script + omit "only runs on Linux" unless RUBY_PLATFORM.include?("linux") + + Tempfile.create(["script", ".sh"]) do |script| + Tempfile.create("crash_report") do |crash_report| + script.write(<<~BASH) + #!/usr/bin/env bash + + cat > #{crash_report.path} + BASH + script.close + + FileUtils.chmod("+x", script) + + assert_crash_report("| #{script.path}") do + assert_include(File.read(crash_report.path), "[BUG] Segmentation fault at") + end + end + end + end + def test_DATA Tempfile.create(["test_ruby_test_rubyoption", ".rb"]) {|t| t.puts "puts DATA.read.inspect" @@ -1292,4 +1308,10 @@ class TestRubyOptions < Test::Unit::TestCase def test_toplevel_ruby assert_instance_of Module, ::Ruby end + + def test_ruby_patchlevel + # We stopped bumping RUBY_PATCHLEVEL at Ruby 4.0.0. + # Released versions have RUBY_PATCHLEVEL 0, and un-released versions have -1. + assert_include [-1, 0], RUBY_PATCHLEVEL + end end diff --git a/test/ruby/test_set.rb b/test/ruby/test_set.rb index af5f65bea0..427dd4b6b0 100644 --- a/test/ruby/test_set.rb +++ b/test/ruby/test_set.rb @@ -3,8 +3,11 @@ require 'test/unit' require 'set' class TC_Set < Test::Unit::TestCase - class Set2 < Set + class SetSubclass < Set end + class CoreSetSubclass < Set::CoreSet + end + ALL_SET_CLASSES = [Set, SetSubclass, CoreSetSubclass].freeze def test_marshal set = Set[1, 2, 3] @@ -81,20 +84,6 @@ class TC_Set < Test::Unit::TestCase s = Set.new(ary) { |o| o * 2 } assert_equal([2,4,6], s.sort) - - assert_raise(ArgumentError) { - Set.new((1..)) - } - assert_raise(ArgumentError) { - Set.new((1..), &:succ) - } - assert_raise(ArgumentError) { - Set.new(1.upto(Float::INFINITY)) - } - - assert_raise(ArgumentError) { - Set.new(Object.new) - } end def test_clone @@ -278,7 +267,7 @@ class TC_Set < Test::Unit::TestCase set.superset?([2]) } - [Set, Set2].each { |klass| + ALL_SET_CLASSES.each { |klass| assert_equal(true, set.superset?(klass[]), klass.name) assert_equal(true, set.superset?(klass[1,2]), klass.name) assert_equal(true, set.superset?(klass[1,2,3]), klass.name) @@ -307,7 +296,7 @@ class TC_Set < Test::Unit::TestCase set.proper_superset?([2]) } - [Set, Set2].each { |klass| + ALL_SET_CLASSES.each { |klass| assert_equal(true, set.proper_superset?(klass[]), klass.name) assert_equal(true, set.proper_superset?(klass[1,2]), klass.name) assert_equal(false, set.proper_superset?(klass[1,2,3]), klass.name) @@ -336,7 +325,7 @@ class TC_Set < Test::Unit::TestCase set.subset?([2]) } - [Set, Set2].each { |klass| + ALL_SET_CLASSES.each { |klass| assert_equal(true, set.subset?(klass[1,2,3,4]), klass.name) assert_equal(true, set.subset?(klass[1,2,3]), klass.name) assert_equal(false, set.subset?(klass[1,2]), klass.name) @@ -365,7 +354,7 @@ class TC_Set < Test::Unit::TestCase set.proper_subset?([2]) } - [Set, Set2].each { |klass| + ALL_SET_CLASSES.each { |klass| assert_equal(true, set.proper_subset?(klass[1,2,3,4]), klass.name) assert_equal(false, set.proper_subset?(klass[1,2,3]), klass.name) assert_equal(false, set.proper_subset?(klass[1,2]), klass.name) @@ -385,7 +374,7 @@ class TC_Set < Test::Unit::TestCase assert_nil(set <=> set.to_a) - [Set, Set2].each { |klass| + ALL_SET_CLASSES.each { |klass| assert_equal(-1, set <=> klass[1,2,3,4], klass.name) assert_equal( 0, set <=> klass[3,2,1] , klass.name) assert_equal(nil, set <=> klass[1,2,4] , klass.name) @@ -701,15 +690,28 @@ class TC_Set < Test::Unit::TestCase end def test_xor - set = Set[1,2,3,4] - ret = set ^ [2,4,5,5] - assert_not_same(set, ret) - assert_equal(Set[1,3,5], ret) + ALL_SET_CLASSES.each { |klass| + set = klass[1,2,3,4] + ret = set ^ [2,4,5,5] + assert_not_same(set, ret) + assert_equal(klass[1,3,5], ret) + + set2 = klass[1,2,3,4] + ret2 = set2 ^ [2,4,5,5] + assert_instance_of(klass, ret2) + assert_equal(klass[1,3,5], ret2) + } + end + + def test_xor_does_not_mutate_other_set + a = Set[1] + b = Set[1, 2] + original_b = b.dup - set2 = Set2[1,2,3,4] - ret2 = set2 ^ [2,4,5,5] - assert_instance_of(Set2, ret2) - assert_equal(Set2[1,3,5], ret2) + result = a ^ b + + assert_equal(original_b, b) + assert_equal(Set[2], result) end def test_eq @@ -861,9 +863,13 @@ class TC_Set < Test::Unit::TestCase set1.add(set2) assert_equal('Set[Set[0], 1, 2, Set[1, 2, Set[...]]]', set2.inspect) - c = Class.new(Set) + c = Class.new(Set::CoreSet) c.set_temporary_name("_MySet") assert_equal('_MySet[1, 2]', c[1, 2].inspect) + + c = Class.new(Set) + c.set_temporary_name("_MySet") + assert_equal('#<_MySet: {1, 2}>', c[1, 2].inspect) end def test_to_s @@ -896,6 +902,25 @@ class TC_Set < Test::Unit::TestCase assert_equal(array.uniq.sort, set.sort) end + def test_compare_by_identity_compact + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + # [Bug #22064] + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + set = Set.new.compare_by_identity + + o = Object.new + set.add(o) + + assert_include(set, o) + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_include(set, o) + end; + end + def test_reset [Set, Class.new(Set)].each { |klass| a = [1, 2] @@ -936,6 +961,27 @@ class TC_Set < Test::Unit::TestCase assert_includes set, i end end + + def test_subclass_new_calls_add + c = Class.new(Set) do + def add(o) + super + super(o+1) + end + end + assert_equal([1, 2], c.new([1]).to_a) + end + + def test_subclass_aref_calls_initialize + c = Class.new(Set) do + def initialize(enum) + super + add(1) + end + end + assert_equal([2, 1], c[2].to_a) + end + end class TC_Enumerable < Test::Unit::TestCase @@ -951,7 +997,38 @@ class TC_Enumerable < Test::Unit::TestCase assert_equal([-10,-8,-6,-4,-2], set.sort) assert_same set, set.to_set - assert_not_same set, set.to_set { |o| o } + transformed = set.to_set { |o| o + 1 } + assert_equal([-9,-7,-5,-3,-1], transformed.sort) + end + + class MyEnum + include Enumerable + + def initialize(array) + @array = array + end + + def each(&block) + @array.each(&block) + end + + def size + raise "should not be called" + end + end + + def test_to_set_not_calling_size + enum = MyEnum.new([1,2,3]) + + set = assert_nothing_raised { enum.to_set } + assert(set.is_a?(Set)) + assert_equal(Set[1,2,3], set) + + enumerator = enum.to_enum + + set = assert_nothing_raised { enumerator.to_set } + assert(set.is_a?(Set)) + assert_equal(Set[1,2,3], set) end end diff --git a/test/ruby/test_settracefunc.rb b/test/ruby/test_settracefunc.rb index fac6dd8185..8b0e08fc97 100644 --- a/test/ruby/test_settracefunc.rb +++ b/test/ruby/test_settracefunc.rb @@ -2226,7 +2226,7 @@ CODE def test_thread_add_trace_func events = [] base_line = __LINE__ - q = Thread::Queue.new + q = [] t = Thread.new{ Thread.current.add_trace_func proc{|ev, file, line, *args| events << [ev, line] if file == __FILE__ @@ -2583,6 +2583,7 @@ CODE def test_enable_target_thread events = [] TracePoint.new(:line) do |tp| + next unless tp.path == __FILE__ events << Thread.current end.enable(target_thread: Thread.current) do _a = 1 @@ -2596,6 +2597,7 @@ CODE events = [] tp = TracePoint.new(:line) do |tp| + next unless tp.path == __FILE__ events << Thread.current end @@ -2724,7 +2726,7 @@ CODE end def test_disable_local_tracepoint_in_trace - assert_normal_exit <<-EOS + assert_normal_exit(<<-EOS, timeout: 60) def foo trace = TracePoint.new(:b_return){|tp| tp.disable @@ -2957,4 +2959,210 @@ CODE assert_kind_of(Thread, target_thread) end + + def test_tracepoint_garbage_collected_when_disable + before_count_stat = 0 + before_count_objspace = 0 + TracePoint.stat.each do + before_count_stat += 1 + end + ObjectSpace.each_object(TracePoint) do + before_count_objspace += 1 + end + tp = TracePoint.new(:c_call, :c_return) do + end + tp.enable + Class.inspect # c_call, c_return invoked + tp.disable + tp_id = tp.object_id + tp = nil + + gc_times = 0 + gc_max_retries = 10 + EnvUtil.suppress_warning do + until (ObjectSpace._id2ref(tp_id) rescue nil).nil? + GC.start + gc_times += 1 + if gc_times == gc_max_retries + break + end + end + end + return if gc_times == gc_max_retries + + after_count_stat = 0 + TracePoint.stat.each do |v| + after_count_stat += 1 + end + assert after_count_stat <= before_count_stat + after_count_objspace = 0 + ObjectSpace.each_object(TracePoint) do + after_count_objspace += 1 + end + assert after_count_objspace <= before_count_objspace + end + + def test_tp_ractor_local_untargeted + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + r = Ractor.new do + results = [] + tp = TracePoint.new(:line) { |tp| results << tp.path } + tp.enable + Ractor.main << :continue + Ractor.receive + tp.disable + results + end + outer_results = [] + outer_tp = TracePoint.new(:line) { |tp| outer_results << tp.path } + outer_tp.enable + Ractor.receive + GC.start # so I can check <internal:gc> path + r << :continue + inner_results = r.value + outer_tp.disable + assert_equal 1, outer_results.select { |path| path.match?(/internal:gc/) }.size + assert_equal 0, inner_results.select { |path| path.match?(/internal:gc/) }.size + end; + end + + def test_tp_targeted_ractor_local_bmethod + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + mname = :foo + prok = Ractor.shareable_proc do + end + klass = EnvUtil.labeled_class(:Klass) do + define_method(mname, &prok) + end + outer_results = 0 + _outer_tp = TracePoint.new(:call) do + outer_results += 1 + end # not enabled + rs = 10.times.map do + Ractor.new(mname, klass) do |mname, klass0| + inner_results = 0 + tp = TracePoint.new(:call) { |tp| inner_results += 1 } + target = klass0.instance_method(mname) + tp.enable(target: target) + obj = klass0.new + 10.times { obj.send(mname) } + tp.disable + inner_results + end + end + inner_results = rs.map(&:value).sum + obj = klass.new + 10.times { obj.send(mname) } + assert_equal 100, inner_results + assert_equal 0, outer_results + end; + end + + def test_tp_targeted_ractor_local_method + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + def foo + end + outer_results = 0 + _outer_tp = TracePoint.new(:call) do + outer_results += 1 + end # not enabled + + rs = 10.times.map do + Ractor.new do + inner_results = 0 + tp = TracePoint.new(:call) do + inner_results += 1 + end + tp.enable(target: method(:foo)) + 10.times { foo } + tp.disable + inner_results + end + end + + inner_results = rs.map(&:value).sum + 10.times { foo } + assert_equal 100, inner_results + assert_equal 0, outer_results + end; + end + + def test_tracepoints_not_disabled_by_ractor_gc + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + $-w = nil # uses ObjectSpace._id2ref + def hi = "hi" + greetings = 0 + tp_target = TracePoint.new(:call) do |tp| + greetings += 1 + end + tp_target.enable(target: method(:hi)) + + raises = 0 + tp_global = TracePoint.new(:raise) do |tp| + raises += 1 + end + tp_global.enable + + r = Ractor.new { 10 } + r.join + ractor_id = r.object_id + r = nil # allow gc for ractor + gc_max_retries = 15 + gc_times = 0 + # force GC of ractor (or try, because we have a conservative GC) + until (ObjectSpace._id2ref(ractor_id) rescue nil).nil? + GC.start + gc_times += 1 + if gc_times == gc_max_retries + break + end + end + + # tracepoints should still be enabled after GC of `r` + 5.times { + hi + } + 6.times { + raise "uh oh" rescue nil + } + tp_target.disable + tp_global.disable + assert_equal 5, greetings + if gc_times == gc_max_retries # _id2ref never raised + assert_equal 6, raises + else + assert_equal 7, raises + end + end; + end + + def test_lots_of_enabled_tracepoints_ractor_gc + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + def foo; end + sum = 8.times.map do + Ractor.new do + called = 0 + TracePoint.new(:call) do |tp| + next if tp.callee_id != :foo + called += 1 + end.enable + 200.times do + TracePoint.new(:line) { + # all these allocations shouldn't GC these tracepoints while the ractor is alive. + Object.new + }.enable + end + 100.times { foo } + called + end + end.map(&:value).sum + assert_equal 800, sum + 4.times { GC.start } # Now the tracepoints can be GC'd because the ractors can be GC'd + end; + end end diff --git a/test/ruby/test_shapes.rb b/test/ruby/test_shapes.rb index 08a841d254..bace69658a 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -2,10 +2,11 @@ require 'test/unit' require 'objspace' require 'json' +require 'securerandom' # These test the functionality of object shapes class TestShapes < Test::Unit::TestCase - MANY_IVS = 80 + MANY_IVS = RubyVM::Shape::SHAPE_MAX_FIELDS + 1 class IVOrder def expected_ivs @@ -94,15 +95,15 @@ class TestShapes < Test::Unit::TestCase # shapes def assert_shape_equal(e, a) assert_equal( - {id: e.id, parent_id: e.parent_id, depth: e.depth, type: e.type}, - {id: a.id, parent_id: a.parent_id, depth: a.depth, type: a.type}, + {id: e.offset, parent_offset: e.parent_offset, depth: e.depth, type: e.type, name: e.edge_name}, + {id: a.offset, parent_offset: a.parent_offset, depth: a.depth, type: a.type, name: e.edge_name}, ) end def refute_shape_equal(e, a) refute_equal( - {id: e.id, parent_id: e.parent_id, depth: e.depth, type: e.type}, - {id: a.id, parent_id: a.parent_id, depth: a.depth, type: a.type}, + {id: e.offset, parent_offset: e.parent_offset, depth: e.depth, type: e.type, name: e.edge_name}, + {id: a.offset, parent_offset: a.parent_offset, depth: a.depth, type: a.type, name: e.edge_name}, ) end @@ -116,12 +117,12 @@ class TestShapes < Test::Unit::TestCase assert_equal obj.expected_ivs, iv_list.map(&:to_s) end - def test_too_complex + def test_complex ensure_complex tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? end def test_ordered_alloc_is_not_complex @@ -130,6 +131,48 @@ class TestShapes < Test::Unit::TestCase assert_operator obj["variation_count"], :<, RubyVM::Shape::SHAPE_MAX_VARIATIONS end + def test_max_iv_count + klass = Class.new + object = klass.new + + assert_equal 0, RubyVM::Shape.class_max_iv_count(klass) + 8.times do |i| + object.instance_variable_set("@ivar_#{i}", i) + end + assert_equal 8, RubyVM::Shape.class_max_iv_count(klass) + + subklass = Class.new(klass) + assert_equal 8, RubyVM::Shape.class_max_iv_count(subklass) + end + + def test_max_iv_count_on_Object + object = Object.new + + assert_equal 0, RubyVM::Shape.class_max_iv_count(Object) + 8.times do |i| + object.instance_variable_set("@ivar_#{i}", i) + end + assert_equal 0, RubyVM::Shape.class_max_iv_count(Object) + end + + def test_max_iv_count_on_BasicObject + object = BasicObject.new + + assert_equal 0, RubyVM::Shape.class_max_iv_count(BasicObject) + 8.times do |i| + Object.instance_method(:instance_variable_set).bind_call(object, "@ivar_#{i}", i) + end + assert_equal 0, RubyVM::Shape.class_max_iv_count(BasicObject) + + subklass = Class.new(BasicObject) + object = subklass.new + assert_equal 0, RubyVM::Shape.class_max_iv_count(subklass) + 8.times do |i| + Object.instance_method(:instance_variable_set).bind_call(object, "@ivar_#{i}", i) + end + assert_equal 8, RubyVM::Shape.class_max_iv_count(subklass) + end + def test_too_many_ivs_on_obj assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; @@ -142,7 +185,7 @@ class TestShapes < Test::Unit::TestCase obj.instance_variable_set(:@c, 1) obj.instance_variable_set(:@d, 1) - assert_predicate RubyVM::Shape.of(obj), :too_complex? + assert_predicate RubyVM::Shape.of(obj), :complex? end; end @@ -150,13 +193,13 @@ class TestShapes < Test::Unit::TestCase obj = Class.new obj.instance_variable_set(:@test_too_many_ivs_on_class, 1) - refute_predicate RubyVM::Shape.of(obj), :too_complex? + refute_predicate RubyVM::Shape.of(obj), :complex? MANY_IVS.times do obj.instance_variable_set(:"@a#{_1}", 1) end - refute_predicate RubyVM::Shape.of(obj), :too_complex? + assert_predicate RubyVM::Shape.of(obj), :complex? end def test_removing_when_too_many_ivs_on_class @@ -185,7 +228,7 @@ class TestShapes < Test::Unit::TestCase assert_empty obj.instance_variables end - def test_too_complex_geniv + def test_complex_geniv assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; class TooComplex < Hash @@ -227,7 +270,7 @@ class TestShapes < Test::Unit::TestCase end def test_run_out_of_shape_for_object - assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; class A def initialize @@ -338,7 +381,7 @@ class TestShapes < Test::Unit::TestCase end def test_gc_stress_during_evacuate_generic_ivar - assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; [].instance_variable_set(:@a, 1) @@ -506,7 +549,7 @@ class TestShapes < Test::Unit::TestCase end def test_run_out_of_shape_rb_obj_copy_ivar - assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; class A def initialize @@ -585,7 +628,7 @@ class TestShapes < Test::Unit::TestCase end; end - def test_too_complex_ractor + def test_complex_ractor assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; $VERBOSE = nil @@ -600,14 +643,14 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.instance_variable_set(:"@very_unique", 3) - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.very_unique assert_equal 3, Ractor.new(tc) { |x| x.very_unique }.value assert_equal tc.instance_variables.sort, Ractor.new(tc) { |x| x.instance_variables }.value.sort end; end - def test_too_complex_ractor_shareable + def test_complex_ractor_shareable assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; $VERBOSE = nil @@ -622,13 +665,13 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.instance_variable_set(:"@very_unique", 3) - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.very_unique assert_equal 3, Ractor.make_shareable(tc).very_unique end; end - def test_too_complex_and_frozen + def test_complex_and_frozen assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; $VERBOSE = nil @@ -644,12 +687,12 @@ class TestShapes < Test::Unit::TestCase tc.instance_variable_set(:"@very_unique", 3) shape = RubyVM::Shape.of(tc) - assert_predicate shape, :too_complex? + assert_predicate shape, :complex? refute_predicate shape, :shape_frozen? tc.freeze frozen_shape = RubyVM::Shape.of(tc) refute_equal shape.id, frozen_shape.id - assert_predicate frozen_shape, :too_complex? + assert_predicate frozen_shape, :complex? assert_predicate frozen_shape, :shape_frozen? assert_equal 3, tc.very_unique @@ -657,7 +700,7 @@ class TestShapes < Test::Unit::TestCase end; end - def test_object_id_transition_too_complex + def test_object_id_transition_complex assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; obj = Object.new @@ -681,7 +724,7 @@ class TestShapes < Test::Unit::TestCase end; end - def test_too_complex_and_frozen_and_object_id + def test_complex_and_frozen_and_object_id assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; $VERBOSE = nil @@ -697,12 +740,12 @@ class TestShapes < Test::Unit::TestCase tc.instance_variable_set(:"@very_unique", 3) shape = RubyVM::Shape.of(tc) - assert_predicate shape, :too_complex? + assert_predicate shape, :complex? refute_predicate shape, :shape_frozen? tc.freeze frozen_shape = RubyVM::Shape.of(tc) refute_equal shape.id, frozen_shape.id - assert_predicate frozen_shape, :too_complex? + assert_predicate frozen_shape, :complex? assert_predicate frozen_shape, :shape_frozen? refute_predicate frozen_shape, :has_object_id? @@ -710,7 +753,7 @@ class TestShapes < Test::Unit::TestCase id_shape = RubyVM::Shape.of(tc) refute_equal frozen_shape.id, id_shape.id - assert_predicate id_shape, :too_complex? + assert_predicate id_shape, :complex? assert_predicate id_shape, :has_object_id? assert_predicate id_shape, :shape_frozen? @@ -719,7 +762,7 @@ class TestShapes < Test::Unit::TestCase end; end - def test_too_complex_obj_ivar_ractor_share + def test_complex_obj_ivar_ractor_share assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; $VERBOSE = nil @@ -737,7 +780,7 @@ class TestShapes < Test::Unit::TestCase end; end - def test_too_complex_generic_ivar_ractor_share + def test_complex_generic_ivar_ractor_share assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; $VERBOSE = nil @@ -760,7 +803,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.a3_m end @@ -769,7 +812,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.a3_m assert_equal 3, tc.a3 end @@ -779,7 +822,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? tc.write_iv_method tc.write_iv_method assert_equal 12345, tc.a3_m @@ -791,7 +834,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? tc.write_iv tc.write_iv assert_equal 12345, tc.a3_m @@ -803,7 +846,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.a3_m assert_equal 3, tc.instance_variable_get(:@a3) end @@ -813,7 +856,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.a3_m # make sure IV is initialized assert tc.instance_variable_defined?(:@a3) @@ -827,7 +870,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.a3_m # make sure IV is initialized assert tc.instance_variable_defined?(:@a3) @@ -842,7 +885,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal 3, tc.a3_m # make sure IV is initialized assert tc.instance_variable_defined?(:@a3) @@ -859,7 +902,7 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? refute tc.instance_variable_defined?(:@a3) assert_raise(NameError) do @@ -953,11 +996,11 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? tc.freeze assert_raise(FrozenError) { tc.a3_m } # doesn't transition to frozen shape in this case - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? end def test_read_undefined_iv_after_complex @@ -965,9 +1008,9 @@ class TestShapes < Test::Unit::TestCase tc = TooComplex.new tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? assert_equal nil, tc.iv_not_defined - assert_predicate RubyVM::Shape.of(tc), :too_complex? + assert_predicate RubyVM::Shape.of(tc), :complex? end def test_shape_order @@ -989,7 +1032,7 @@ class TestShapes < Test::Unit::TestCase example.add_foo # makes a transition add_foo_shape = RubyVM::Shape.of(example) assert_equal([:@foo], example.instance_variables) - assert_equal(initial_shape.raw_id, add_foo_shape.parent.raw_id) + assert_equal(initial_shape.offset, add_foo_shape.parent.offset) assert_equal(1, add_foo_shape.next_field_index) example.remove_foo # makes a transition @@ -1000,7 +1043,7 @@ class TestShapes < Test::Unit::TestCase example.add_bar # makes a transition bar_shape = RubyVM::Shape.of(example) assert_equal([:@bar], example.instance_variables) - assert_equal(initial_shape.raw_id, bar_shape.parent_id) + assert_equal(initial_shape.offset, bar_shape.parent_offset) assert_equal(1, bar_shape.next_field_index) end @@ -1024,6 +1067,37 @@ class TestShapes < Test::Unit::TestCase assert_nil shape.parent end + def test_shape_layout + assert_equal :robject, RubyVM::Shape.of(TestObject.new).layout + + if ENV["RUBY_BOX"] + assert_equal :other, RubyVM::Shape.of(Kernel).layout + assert_equal :other, RubyVM::Shape.of(String).layout + else + assert_equal :rclass, RubyVM::Shape.of(Kernel).layout + assert_equal :rclass, RubyVM::Shape.of(String).layout + end + + assert_equal :rclass, RubyVM::Shape.of(Class.new).layout + assert_equal :rclass, RubyVM::Shape.of(Module.new).layout + + klass = Class.new + assert_equal :rclass, RubyVM::Shape.of(klass).layout + klass.instance_variable_set(:@a, 123) + assert_equal :rclass, RubyVM::Shape.of(klass).layout + + assert_equal :rdata, RubyVM::Shape.of(Thread.current).layout + assert_equal :rdata, RubyVM::Shape.of(lambda {}).layout + + assert_equal :other, RubyVM::Shape.of(Struct.new(:x).new(1)).layout + assert_equal :other, RubyVM::Shape.of([]).layout + assert_equal :other, RubyVM::Shape.of("hello").layout + assert_equal :other, RubyVM::Shape.of(/foo/).layout + assert_equal :other, RubyVM::Shape.of(2..3).layout + assert_equal :other, RubyVM::Shape.of(2**67).layout + assert_equal :other, RubyVM::Shape.of(:"aaroniscool#{123}").layout + end + def test_str_has_root_shape assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of("")) end @@ -1054,7 +1128,7 @@ class TestShapes < Test::Unit::TestCase def test_root_shape_frozen frozen_root_shape = RubyVM::Shape.of([].freeze) assert_predicate(frozen_root_shape, :frozen?) - assert_equal(RubyVM::Shape.root_shape.id, frozen_root_shape.raw_id) + assert_equal(RubyVM::Shape.root_shape.id, frozen_root_shape.offset) end def test_basic_shape_transition @@ -1085,7 +1159,7 @@ class TestShapes < Test::Unit::TestCase assert_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) end - def test_duplicating_too_complex_objects_memory_leak + def test_duplicating_complex_objects_memory_leak assert_no_memory_leak([], "#{<<~'begin;'}", "#{<<~'end;'}", "[Bug #20162]", rss: true) RubyVM::Shape.exhaust_shapes @@ -1112,7 +1186,7 @@ class TestShapes < Test::Unit::TestCase obj = Example.new.freeze obj2 = obj.dup refute_predicate(obj2, :frozen?) - refute_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) + refute_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) assert_equal(obj2.instance_variable_get(:@a), 1) end @@ -1138,7 +1212,7 @@ class TestShapes < Test::Unit::TestCase obj = Object.new obj2 = obj.clone(freeze: true) assert_predicate(obj2, :frozen?) - refute_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) + refute_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) assert_predicate(RubyVM::Shape.of(obj2), :shape_frozen?) end @@ -1182,4 +1256,68 @@ class TestShapes < Test::Unit::TestCase tc.send("a#{_1}_m") end end + + def assert_complex_during_delete(obj) + obj.instance_variable_set("@___#{SecureRandom.hex}", 1) + + (RubyVM::Shape::SHAPE_MAX_VARIATIONS * 2).times do |i| + obj.instance_variable_set("@ivar#{i}", i) + end + + refute_predicate RubyVM::Shape.of(obj), :complex? + (RubyVM::Shape::SHAPE_MAX_VARIATIONS * 2).times do |i| + obj.remove_instance_variable("@ivar#{i}") + end + assert_predicate RubyVM::Shape.of(obj), :complex? + end + + def test_object_complex_during_delete + assert_complex_during_delete(Class.new.new) + end + + def test_class_complex_during_delete + assert_complex_during_delete(Module.new) + end + + def test_generic_complex_during_delete + assert_complex_during_delete(Class.new(Array).new) + end + + def assert_complex_max_fields(obj) + extra_fields = RubyVM::Shape::SHAPE_MAX_FIELDS - obj.instance_variables.size + extra_fields.times do |i| + obj.instance_variable_set("@camel_ivar#{i}", i) + end + refute_predicate RubyVM::Shape.of(obj), :complex? + obj.instance_variable_set("@camel_straw", true) + assert_predicate RubyVM::Shape.of(obj), :complex? + end + + def test_max_fields_complex + assert_complex_max_fields(Class.new(Object).new) + end + + def test_generic_max_fields_complex + assert_complex_max_fields(Class.new(Array).new) + end + + def test_class_max_fields_complex + assert_complex_max_fields(Class.new(Module).new) + end + + def test_max_initial_fields + klass = Class.new + init_ivars = (RubyVM::Shape::SHAPE_MAX_FIELDS + 1).times.map { |i| "@ivar_#{i} = #{i}" } + klass.class_eval(<<~RUBY) + def initialize(init = false) + if init + #{init_ivars.join(";")} + end + end + RUBY + assert_predicate RubyVM::Shape.of(klass.new), :complex? + assert_predicate RubyVM::Shape.of(klass.new.dup), :complex? + assert_predicate RubyVM::Shape.of(klass.new(true)), :complex? + assert_predicate RubyVM::Shape.of(klass.new(true).dup), :complex? + end end if defined?(RubyVM::Shape) diff --git a/test/ruby/test_signal.rb b/test/ruby/test_signal.rb index a2bdf02b88..1ee3720ded 100644 --- a/test/ruby/test_signal.rb +++ b/test/ruby/test_signal.rb @@ -320,20 +320,20 @@ class TestSignal < Test::Unit::TestCase # The parent should be notified about the stop _, status = Process.waitpid2(child_pid, Process::WUNTRACED) - assert status.stopped? + assert_predicate status, :stopped? # It can be continued Process.kill(:CONT, child_pid) # And the child then runs to completion _, status = Process.waitpid2(child_pid) - assert status.exited? - assert status.success? + assert_predicate status, :exited? + assert_predicate status, :success? end def test_sigwait_fd_unused t = EnvUtil.apply_timeout_scale(0.1) - assert_separately([], <<-End) + assert_ruby_status([], <<-End) tgt = $$ trap(:TERM) { exit(0) } e = "Process.daemon; sleep #{t * 2}; Process.kill(:TERM,\#{tgt})" @@ -350,4 +350,18 @@ class TestSignal < Test::Unit::TestCase loop { sleep } End end if Process.respond_to?(:kill) && Process.respond_to?(:daemon) + + def test_signal_during_kwarg_call + status = assert_in_out_err([], <<~'RUBY', [], [], success: false) + Thread.new do + sleep 0.1 + Process.kill("TERM", $$) + end + + loop do + File.open(IO::NULL, kwarg: true) {} + end + RUBY + assert_predicate(status, :signaled?) if Signal.list.include?("QUIT") + end if Process.respond_to?(:kill) end diff --git a/test/ruby/test_sleep.rb b/test/ruby/test_sleep.rb index 991b73ebd5..7ef962db4a 100644 --- a/test/ruby/test_sleep.rb +++ b/test/ruby/test_sleep.rb @@ -1,6 +1,7 @@ # frozen_string_literal: false require 'test/unit' require 'etc' +require 'timeout' class TestSleep < Test::Unit::TestCase def test_sleep_5sec @@ -13,4 +14,21 @@ class TestSleep < Test::Unit::TestCase assert_operator(slept, :<=, 6.0, "[ruby-core:18015]: longer than expected") end end + + def test_sleep_forever_not_woken_by_sigchld + begin + t = Thread.new do + sleep 0.5 + `echo hello` + end + + assert_raise Timeout::Error do + Timeout.timeout 2 do + sleep # Should block forever + end + end + ensure + t.join + end + end end diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 1e0f31ba7c..aedfc93e5d 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -675,7 +675,7 @@ CODE omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 require 'objspace' - base_slot_size = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + base_slot_size = GC.stat_heap(0, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] small_obj_size = (base_slot_size / 2) large_obj_size = base_slot_size * 2 @@ -851,7 +851,6 @@ CODE assert_equal(S("\u{AB}"), S('"\\u00AB"').undump) assert_equal(S("\u{ABC}"), S('"\\u0ABC"').undump) assert_equal(S("\uABCD"), S('"\\uABCD"').undump) - assert_equal(S("\uABCD"), S('"\\uABCD"').undump) assert_equal(S("\u{ABCDE}"), S('"\\u{ABCDE}"').undump) assert_equal(S("\u{10ABCD}"), S('"\\u{10ABCD}"').undump) assert_equal(S("\u{ABCDE 10ABCD}"), S('"\\u{ABCDE 10ABCD}"').undump) @@ -872,6 +871,10 @@ CODE assert_equal('\#', S('"\\\\#"').undump) assert_equal('\#{', S('"\\\\\#{"').undump) + assert_undump("\0\u{ABCD}") + assert_undump(S('"\x00\u3042"'.force_encoding("SJIS"))) + assert_undump(S('"\u3042\x7E"'.force_encoding("SJIS"))) + assert_raise(RuntimeError) { S('\u3042').undump } assert_raise(RuntimeError) { S('"\x82\xA0\u3042"'.force_encoding("SJIS")).undump } assert_raise(RuntimeError) { S('"\u3042\x82\xA0"'.force_encoding("SJIS")).undump } @@ -994,6 +997,32 @@ CODE assert_equal [65, 66, 67], res end + def test_getbyte + s = S('foo') + assert_equal(102, s.getbyte(0)) + assert_equal(111, s.getbyte(2)) + assert_equal(102, s.getbyte(-3)) + assert_nil(s.getbyte(3)) + assert_nil(s.getbyte(-4)) + assert_nil(S('').getbyte(0)) + assert_nil(S('').getbyte(-1)) + end + + def test_setbyte + s = S('xyzzy') + assert_equal(129, s.setbyte(2, 129)) + assert_equal(S("xy\x81zy").force_encoding(s.encoding), s) + + s = S('foo') + s.setbyte(-3, 98) + assert_equal(S('boo').force_encoding(s.encoding), s) + + assert_raise(IndexError) { S('foo').setbyte(3, 0) } + assert_raise(IndexError) { S('foo').setbyte(-4, 0) } + + assert_raise(FrozenError) { S('foo').freeze.setbyte(0, 0x61) } + end + def test_each_codepoint # Single byte optimization assert_equal 65, S("ABC").each_codepoint.next @@ -1883,9 +1912,24 @@ CODE def test_fs return unless @cls == String - assert_raise_with_message(TypeError, /\$;/) { - $; = [] - } + begin + fs = $; + assert_deprecated_warning(/non-nil '\$;'/) {$; = "x"} + assert_raise_with_message(TypeError, /\$;/) {$; = []} + ensure + EnvUtil.suppress_warning {$; = fs} + end + name = "\u{5206 5217}" + assert_separately([], "#{<<~"do;"}\n#{<<~"end;"}") + do; + alias $#{name} $; + assert_deprecated_warning(/\\$#{name}/) { $#{name} = "" } + assert_raise_with_message(TypeError, /\\$#{name}/) { $#{name} = 1 } + end; + end + + def test_fs_gc + return unless @cls == String assert_separately(%W[-W0], "#{<<~"begin;"}\n#{<<~'end;'}") bug = '[ruby-core:79582] $; must not be GCed' @@ -2034,6 +2078,117 @@ CODE assert_equal(S("x") ,a) end + def test_strip_with_selectors + assert_equal(S("abc"), S("---abc+++").strip("-+")) + assert_equal(S("abc"), S("+++abc---").strip("-+")) + assert_equal(S("abc"), S("+-+abc-+-").strip("-+")) + assert_equal(S(""), S("---+++").strip("-+")) + assert_equal(S("abc "), S("---abc ").strip("-")) + assert_equal(S(" abc"), S(" abc+++").strip("+")) + + # Test with multibyte characters + assert_equal(S("abc"), S("あああabcいいい").strip("あい")) + assert_equal(S("abc"), S("いいいabcあああ").strip("あい")) + + # Test with NUL characters + assert_equal(S("abc\0"), S("---abc\0--").strip("-")) + assert_equal(S("\0abc"), S("--\0abc---").strip("-")) + + # Test without modification + assert_equal(S("abc"), S("abc").strip("-+")) + assert_equal(S("abc"), S("abc").strip("")) + + # Test with range + assert_equal(S("abc"), S("012abc345").strip("0-9")) + assert_equal(S("abc"), S("012abc345").strip("^a-z")) + + # Test with multiple selectors + assert_equal(S("4abc56"), S("01234abc56789").strip("0-9", "^4-6")) + end + + def test_strip_bang_with_chars + a = S("---abc+++") + assert_equal(S("abc"), a.strip!("-+")) + assert_equal(S("abc"), a) + + a = S("+++abc---") + assert_equal(S("abc"), a.strip!("-+")) + assert_equal(S("abc"), a) + + a = S("abc") + assert_nil(a.strip!("-+")) + assert_equal(S("abc"), a) + + # Test with multibyte characters + a = S("あああabcいいい") + assert_equal(S("abc"), a.strip!("あい")) + assert_equal(S("abc"), a) + end + + def test_lstrip_with_selectors + assert_equal(S("abc+++"), S("---abc+++").lstrip("-")) + assert_equal(S("abc---"), S("+++abc---").lstrip("+")) + assert_equal(S("abc"), S("---abc").lstrip("-")) + assert_equal(S(""), S("---").lstrip("-")) + + # Test with multibyte characters + assert_equal(S("abcいいい"), S("あああabcいいい").lstrip("あ")) + + # Test with NUL characters + assert_equal(S("\0abc+++"), S("--\0abc+++").lstrip("-")) + + # Test without modification + assert_equal(S("abc"), S("abc").lstrip("-")) + + # Test with range + assert_equal(S("abc345"), S("012abc345").lstrip("0-9")) + + # Test with multiple selectors + assert_equal(S("4abc56789"), S("01234abc56789").lstrip("0-9", "^4-6")) + end + + def test_lstrip_bang_with_chars + a = S("---abc+++") + assert_equal(S("abc+++"), a.lstrip!("-")) + assert_equal(S("abc+++"), a) + + a = S("abc") + assert_nil(a.lstrip!("-")) + assert_equal(S("abc"), a) + end + + def test_rstrip_with_selectors + assert_equal(S("---abc"), S("---abc+++").rstrip("+")) + assert_equal(S("+++abc"), S("+++abc---").rstrip("-")) + assert_equal(S("abc"), S("abc+++").rstrip("+")) + assert_equal(S(""), S("+++").rstrip("+")) + + # Test with multibyte characters + assert_equal(S("あああabc"), S("あああabcいいい").rstrip("い")) + + # Test with NUL characters + assert_equal(S("---abc\0"), S("---abc\0++").rstrip("+")) + + # Test without modification + assert_equal(S("abc"), S("abc").rstrip("-")) + + # Test with range + assert_equal(S("012abc"), S("012abc345").rstrip("0-9")) + + # Test with multiple selectors + assert_equal(S("01234abc56"), S("01234abc56789").rstrip("0-9", "^4-6")) + end + + def test_rstrip_bang_with_chars + a = S("---abc+++") + assert_equal(S("---abc"), a.rstrip!("+")) + assert_equal(S("---abc"), a) + + a = S("abc") + assert_nil(a.rstrip!("+")) + assert_equal(S("abc"), a) + end + def test_sub assert_equal(S("h*llo"), S("hello").sub(/[aeiou]/, S('*'))) assert_equal(S("h<e>llo"), S("hello").sub(/([aeiou])/, S('<\1>'))) @@ -2473,7 +2628,7 @@ CODE end def test_upcase - assert_equal(S("HELLO"), S("hello").upcase) + assert_equal(S("HELLO"), S("helLO").upcase) assert_equal(S("HELLO"), S("hello").upcase) assert_equal(S("HELLO"), S("HELLO").upcase) assert_equal(S("ABC HELLO 123"), S("abc HELLO 123").upcase) @@ -2627,8 +2782,9 @@ CODE def test_match_method assert_equal("bar", S("foobarbaz").match(/bar/).to_s) - o = Regexp.new('foo') - def o.match(x, y, z); x + y + z; end + o = Class.new(Regexp) { + def match(x, y, z) = x + y + z + }.new('foo') assert_equal("foobarbaz", S("foo").match(o, "bar", "baz")) x = nil S("foo").match(o, "bar", "baz") {|y| x = y } @@ -2761,14 +2917,21 @@ CODE assert_equal([S("abcdb"), S("c"), S("e")], S("abcdbce").rpartition(/b\Kc/)) end - def test_fs_setter + def test_rs return unless @cls == String - assert_raise(TypeError) { $/ = 1 } + begin + rs = $/ + assert_deprecated_warning(/non-nil '\$\/'/) { $/ = "" } + assert_raise(TypeError) { $/ = 1 } + ensure + EnvUtil.suppress_warning { $/ = rs } + end name = "\u{5206 884c}" assert_separately([], "#{<<~"do;"}\n#{<<~"end;"}") do; alias $#{name} $/ + assert_deprecated_warning(/\\$#{name}/) { $#{name} = "" } assert_raise_with_message(TypeError, /\\$#{name}/) { $#{name} = 1 } end; end @@ -3323,6 +3486,27 @@ CODE assert_equal(false, ("\u3042"*10).byteslice(0, 20).valid_encoding?, bug7954) end + def test_shared_middle_string_terminator + ten = "0123456789" + hundred = ten * 10 + str = "#{hundred}\0#{hundred}".freeze + + require 'objspace' + + substr = str.byteslice(0, hundred.bytesize) + assert_equal hundred, substr + assert_includes ObjectSpace.dump(substr), ' "shared":true,' + + # Larger terminator + substr.force_encoding(Encoding::UTF_16BE) + assert_equal hundred.dup.force_encoding(Encoding::UTF_16BE), substr + refute_includes ObjectSpace.dump(substr), ' "shared":true,' + + substr = str.byteslice(0, hundred.bytesize + 1) + assert_equal hundred + "\0", substr + refute_includes ObjectSpace.dump(substr), ' "shared":true,' + end + def test_unknown_string_option str = nil assert_nothing_raised(SyntaxError) do @@ -3441,6 +3625,17 @@ CODE assert_equal(false, str.frozen?) end + def test_uminus_no_embed_gc + pad = "a"*2048 + File.open(IO::NULL, "w") do |dev_null| + ("aa".."zz").each do |c| + fstr = -(c + pad).freeze + dev_null.write(fstr) + end + end + GC.start + end + def test_ord assert_equal(97, S("a").ord) assert_equal(97, S("abc").ord) @@ -3747,6 +3942,96 @@ CODE Warning[:deprecated] = deprecated end + def test_encode_fallback_raise_memory_leak + { + "hash" => <<~RUBY, + fallback = Hash.new { raise MyError } + RUBY + "proc" => <<~RUBY, + fallback = proc { raise MyError } + RUBY + "method" => <<~RUBY, + def my_method(_str) = raise MyError + fallback = method(:my_method) + RUBY + "aref" => <<~RUBY, + fallback = Object.new + def fallback.[](_str) = raise MyError + RUBY + }.each do |type, code| + assert_no_memory_leak([], '', <<~RUBY, "fallback type is #{type}", rss: true) + class MyError < StandardError; end + + #{code} + + 100_000.times do |i| + "\\ufffd".encode(Encoding::US_ASCII, fallback:) + rescue MyError + end + RUBY + end + end + + def test_encode_fallback_too_big_memory_leak + { + "hash" => <<~RUBY, + fallback = Hash.new { "\\uffee" } + RUBY + "proc" => <<~RUBY, + fallback = proc { "\\uffee" } + RUBY + "method" => <<~RUBY, + def my_method(_str) = "\\uffee" + fallback = method(:my_method) + RUBY + "aref" => <<~RUBY, + fallback = Object.new + def fallback.[](_str) = "\\uffee" + RUBY + }.each do |type, code| + assert_no_memory_leak([], '', <<~RUBY, "fallback type is #{type}", rss: true) + class MyError < StandardError; end + + #{code} + + 100_000.times do |i| + "\\ufffd".encode(Encoding::US_ASCII, fallback:) + rescue ArgumentError + end + RUBY + end + end + + def test_encode_fallback_not_string_memory_leak + { + "hash" => <<~RUBY, + fallback = Hash.new { Object.new } + RUBY + "proc" => <<~RUBY, + fallback = proc { Object.new } + RUBY + "method" => <<~RUBY, + def my_method(_str) = Object.new + fallback = method(:my_method) + RUBY + "aref" => <<~RUBY, + fallback = Object.new + def fallback.[](_str) = Object.new + RUBY + }.each do |type, code| + assert_no_memory_leak([], '', <<~RUBY, "fallback type is #{type}", rss: true) + class MyError < StandardError; end + + #{code} + + 100_000.times do |i| + "\\ufffd".encode(Encoding::US_ASCII, fallback:) + rescue TypeError + end + RUBY + end + end + private def assert_bytesplice_result(expected, s, *args) @@ -3793,6 +4078,10 @@ CODE def assert_byterindex(expected, string, match, *rest) assert_index_like(:byterindex, expected, string, match, *rest) end + + def assert_undump(str, *rest) + assert_equal(str, str.dump.undump, *rest) + end end class TestString2 < TestString diff --git a/test/ruby/test_struct.rb b/test/ruby/test_struct.rb index db591c306e..b37f4dba97 100644 --- a/test/ruby/test_struct.rb +++ b/test/ruby/test_struct.rb @@ -41,8 +41,14 @@ module TestStruct end end + MAX_EMBEDDED_MEMBERS = ( + GC::INTERNAL_CONSTANTS[:RVARGC_MAX_ALLOCATE_SIZE] - + GC::INTERNAL_CONSTANTS[:RBASIC_SIZE] - + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] + ) / RbConfig::SIZEOF["void*"] + def test_larger_than_largest_pool - count = (GC::INTERNAL_CONSTANTS[:RVARGC_MAX_ALLOCATE_SIZE] / RbConfig::SIZEOF["void*"]) + 1 + count = MAX_EMBEDDED_MEMBERS + 1 list = Array(0..count) klass = @Struct.new(*list.map { |i| :"a_#{i}"}) struct = klass.new(*list) @@ -538,13 +544,13 @@ module TestStruct omit 'skip on riscv64-linux CI machine. See https://github.com/ruby/ruby/pull/13422' if ENV['RUBY_DEBUG'] == 'ci' && /riscv64-linux/ =~ RUBY_DESCRIPTION # [Bug #20311] - assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true) + assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true, limit: 2.2) code = proc do Struct.new("A") Struct.send(:remove_const, :A) end - 1_000.times(&code) + 10_000.times(&code) PREP 50_000.times(&code) CODE diff --git a/test/ruby/test_super.rb b/test/ruby/test_super.rb index 8e973b0f7f..39594d74be 100644 --- a/test/ruby/test_super.rb +++ b/test/ruby/test_super.rb @@ -759,4 +759,33 @@ class TestSuper < Test::Unit::TestCase inherited = inherited_class.new assert_equal 2, inherited.test # it may read index=1 while it should be index=2 end + + def test_define_initialize_in_basic_object + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class ::BasicObject + alias_method :initialize, :initialize + def initialize + @bug = "[Bug #21992]" + end + end + + assert_not_nil Object.new + end; + end + + def test_super_in_basic_object + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class ::BasicObject + def no_super + super() + rescue ::NameError + :ok + end + end + + assert_equal :ok, "[Bug #21694]".no_super + end; + end end diff --git a/test/ruby/test_symbol.rb b/test/ruby/test_symbol.rb index c50febf5d1..fa65dca225 100644 --- a/test/ruby/test_symbol.rb +++ b/test/ruby/test_symbol.rb @@ -417,8 +417,9 @@ class TestSymbol < Test::Unit::TestCase def test_match_method assert_equal("bar", :"foobarbaz".match(/bar/).to_s) - o = Regexp.new('foo') - def o.match(x, y, z); x + y + z; end + o = Class.new(Regexp) { + def match(x, y, z) = x + y + z + }.new('foo') assert_equal("foobarbaz", :"foo".match(o, "bar", "baz")) x = nil :"foo".match(o, "bar", "baz") {|y| x = y } diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index b7e021a4ff..ae4cdf5fe7 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -202,6 +202,59 @@ class TestSyntax < Test::Unit::TestCase assert_syntax_error("def f(...); g(&); end", /no anonymous block parameter/) end + def test_no_block_argument_in_method + assert_valid_syntax("def f(&nil) end") + assert_valid_syntax("def f(a, &nil) end") + assert_valid_syntax("def f(*rest, &nil) end") + assert_valid_syntax("def f(*rest, p, &nil) end") + assert_valid_syntax("def f(a, *rest, &nil) end") + assert_valid_syntax("def f(a, *rest, p, &nil) end") + assert_valid_syntax("def f(a, k: nil, &nil) end") + assert_valid_syntax("def f(a, k: nil, **kw, &nil) end") + assert_valid_syntax("def f(a, *rest, k: nil, &nil) end") + assert_valid_syntax("def f(a, *rest, k: nil, **kw, &nil) end") + assert_valid_syntax("def f(a, *rest, p, k: nil, &nil) end") + assert_valid_syntax("def f(a, *rest, p, k: nil, **kw, &nil) end") + + obj = Object.new + obj.instance_eval "def f(&nil) end" + assert_raise_with_message(ArgumentError, /block accepted/) {obj.f {}} + assert_raise_with_message(ArgumentError, /block accepted/) {obj.f(&proc {})} + end + + def test_trailing_comma_in_method_parameters + assert_valid_syntax("def f(a,b,c,); end") + assert_valid_syntax("def f(a,b,*c,); end") + assert_valid_syntax("def f(a,b,*,); end") + assert_valid_syntax("def f(a,b,**c,); end") + assert_valid_syntax("def f(a,b,**,); end") + assert_syntax_error("def f(a,b,&block,); end", /unexpected/) + assert_syntax_error("def f(a,b,...,); end", /unexpected/) + end + + def test_no_block_argument_in_block + assert_valid_syntax("proc do |&nil| end") + assert_valid_syntax("proc do |a, &nil| end") + assert_valid_syntax("proc do |*rest, &nil| end") + assert_valid_syntax("proc do |*rest, p, &nil| end") + assert_valid_syntax("proc do |a, *rest, &nil| end") + assert_valid_syntax("proc do |a, *rest, p, &nil| end") + assert_valid_syntax("proc do |a, k: nil, &nil| end") + assert_valid_syntax("proc do |a, k: nil, **kw, &nil| end") + assert_valid_syntax("proc do |a, *rest, k: nil, &nil| end") + assert_valid_syntax("proc do |a, *rest, k: nil, **kw, &nil| end") + assert_valid_syntax("proc do |a, *rest, p, k: nil, &nil| end") + assert_valid_syntax("proc do |a, *rest, p, k: nil, **kw, &nil| end") + + pr = eval "proc {|&nil|}" + assert_nil(pr.call) + assert_raise_with_message(ArgumentError, /block accepted/) {pr.call {}} + pr = eval "proc {|a, &nil| a}" + assert_nil(pr.call) + assert_equal(1, pr.call(1)) + assert_raise_with_message(ArgumentError, /block accepted/) {pr.call {}} + end + def test_newline_in_block_parameters bug = '[ruby-dev:45292]' ["", "a", "a, b"].product(["", ";x", [";", "x"]]) do |params| @@ -1259,6 +1312,52 @@ eom assert_valid_syntax("a #\n#\n&.foo\n") end + def test_fluent_and + assert_valid_syntax("a\n" "&& foo") + assert_valid_syntax("a\n" "and foo") + + assert_equal(:ok, eval("#{<<~"begin;"}\n#{<<~'end;'}")) + begin; + a = true + if a + && (a = :ok; true) + a + end + end; + + assert_equal(:ok, eval("#{<<~"begin;"}\n#{<<~'end;'}")) + begin; + a = true + if a + and (a = :ok; true) + a + end + end; + end + + def test_fluent_or + assert_valid_syntax("a\n" "|| foo") + assert_valid_syntax("a\n" "or foo") + + assert_equal(:ok, eval("#{<<~"begin;"}\n#{<<~'end;'}")) + begin; + a = false + if a + || (a = :ok; true) + a + end + end; + + assert_equal(:ok, eval("#{<<~"begin;"}\n#{<<~'end;'}")) + begin; + a = false + if a + or (a = :ok; true) + a + end + end; + end + def test_safe_call_in_massign_lhs assert_syntax_error("*a&.x=0", /multiple assignment destination/) assert_syntax_error("a&.x,=0", /multiple assignment destination/) @@ -1493,10 +1592,10 @@ eom begin raise; rescue; return; end return false; raise return 1; raise - "#{return}" - raise((return; "should not raise")) + "#{return if true}" + raise((return if true; "should not raise")) begin raise; ensure return; end; self - nil&defined?0--begin e=no_method_error(); return; 0;end + nil&defined?0--begin e=no_method_error(); return if true; 0;end return puts('ignored') #=> ignored BEGIN {return} END {return if false} @@ -1794,15 +1893,12 @@ eom assert_equal("class ok", k.rescued("ok")) assert_equal("instance ok", k.new.rescued("ok")) - # Current technical limitation: cannot prepend "private" or something for command endless def - error = /(syntax error,|\^~*) unexpected string literal/ - error2 = /(syntax error,|\^~*) unexpected local variable or method/ - assert_syntax_error('private def foo = puts "Hello"', error) - assert_syntax_error('private def foo() = puts "Hello"', error) - assert_syntax_error('private def foo(x) = puts x', error2) - assert_syntax_error('private def obj.foo = puts "Hello"', error) - assert_syntax_error('private def obj.foo() = puts "Hello"', error) - assert_syntax_error('private def obj.foo(x) = puts x', error2) + assert_valid_syntax('private def foo = puts "Hello"') + assert_valid_syntax('private def foo() = puts "Hello"') + assert_valid_syntax('private def foo(x) = puts x') + assert_valid_syntax('private def obj.foo = puts "Hello"') + assert_valid_syntax('private def obj.foo() = puts "Hello"') + assert_valid_syntax('private def obj.foo(x) = puts x') end def test_methoddef_in_cond @@ -1815,6 +1911,24 @@ eom assert_valid_syntax('while class Foo a = tap do end; end; break; end') end + def test_while_until_conditional_bug_22002 + @foo = 123 until defined?(@foo) + assert_equal(123, @foo) + + @bar = 456 while @bar==nil..true + assert_equal(456, @bar) + + while false and @baz + @baz = 789 + end + assert_equal(nil, @baz) + + until true || @baz + @baz = 789 + end + assert_equal(nil, @baz) + end + def test_command_with_cmd_brace_block assert_valid_syntax('obj.foo (1) {}') assert_valid_syntax('obj::foo (1) {}') @@ -1957,12 +2071,82 @@ eom assert_equal(/9/, eval('9.then { /#{it}/o }')) end + def test_it_with_splat_super_method + bug21256 = '[ruby-core:121592] [Bug #21256]' + + a = Class.new do + define_method(:foo) { it } + end + b = Class.new(a) do + def foo(*args) = super + end + + assert_equal(1, b.new.foo(1), bug21256) + end + + BUG_21669 = '[Bug #21669]' + + def test_value_expr_in_block + assert_syntax_error("#{<<~"{#"}\n#{<<~'};'}", /void value expression/, nil, "#{BUG_21669} 2.1") + {# + x = begin + return + "NG" + end + }; + end + def test_value_expr_in_condition mesg = /void value expression/ assert_syntax_error("tap {a = (true ? next : break)}", mesg) assert_valid_syntax("tap {a = (true ? true : break)}") assert_valid_syntax("tap {a = (break if false)}") assert_valid_syntax("tap {a = (break unless true)}") + + assert_syntax_error("#{<<~"{#"}\n#{<<~'};'}", /void value expression/, nil, "#{BUG_21669} 1.4") + {# + x = if rand < 0.5 + return + else + return + end + }; + + assert_syntax_error("#{<<~"{#"}\n#{<<~'};'}", /void value expression/, nil, "#{BUG_21669} 2.2") + {# + x = if rand < 0.5 + return + "NG" + else + return + end + }; + + assert_valid_syntax("#{<<~"{#"}\n#{<<~'};'}", "#{BUG_21669} 2.3") + {# + x = begin + return if true + "OK" + end + }; + + assert_valid_syntax("#{<<~"{#"}\n#{<<~'};'}") + {# + x = if true + return "NG" + else + "OK" + end + }; + + assert_valid_syntax("#{<<~"{#"}\n#{<<~'};'}") + {# + x = if false + "OK" + else + return "NG" + end + }; end def test_value_expr_in_singleton @@ -1970,6 +2154,67 @@ eom assert_syntax_error("class << (return); end", mesg) end + def test_value_expr_in_rescue + assert_valid_syntax("#{<<~"{#"}\n#{<<~'};'}", "#{BUG_21669} 1.1") + {# + x = begin + raise + return + rescue + "OK" + else + return + end + }; + + assert_syntax_error("#{<<~"{#"}\n#{<<~'};'}", /void value expression/, nil, "#{BUG_21669} 1.2") + {# + x = begin + foo + rescue + return + else + return + end + }; + end + + def test_value_expr_in_case + assert_syntax_error("#{<<~"{#"}\n#{<<~'};'}", /void value expression/, nil, "#{BUG_21669} 1.3") + {# + x = + case a + when 1; return + when 2; return + else return + end + }; + end + + def test_value_expr_in_case2 + assert_syntax_error("#{<<~"{#"}\n#{<<~'};'}", /void value expression/, nil, "#{BUG_21669} 1.3") + {# + x = + case + when 1; return + when 2; return + else return + end + }; + end + + def test_value_expr_in_case3 + assert_syntax_error("#{<<~"{#"}\n#{<<~'};'}", /void value expression/, nil, "#{BUG_21669} 1.3") + {# + x = + case a + in 1; return + in 2; return + else return + end + }; + end + def test_tautological_condition assert_valid_syntax("def f() return if false and invalid; nil end") assert_valid_syntax("def f() return unless true or invalid; nil end") @@ -2027,10 +2272,11 @@ eom end obj4 = obj1.clone obj5 = obj1.clone + obj6 = obj1.clone obj1.instance_eval('def foo(...) bar(...) end', __FILE__, __LINE__) - obj1.instance_eval('def foo(...) eval("bar(...)") end', __FILE__, __LINE__) obj4.instance_eval("def foo ...\n bar(...)\n""end", __FILE__, __LINE__) obj5.instance_eval("def foo ...; bar(...); end", __FILE__, __LINE__) + obj6.instance_eval('def foo(...) eval("bar(...)") end', __FILE__, __LINE__) klass = Class.new { def foo(*args, **kws, &block) @@ -2059,7 +2305,7 @@ eom end obj3.instance_eval('def foo(...) bar(...) end', __FILE__, __LINE__) - [obj1, obj2, obj3, obj4, obj5].each do |obj| + [obj1, obj2, obj3, obj4, obj5, obj6].each do |obj| assert_warning('') { assert_equal([[1, 2, 3], {k1: 4, k2: 5}], obj.foo(1, 2, 3, k1: 4, k2: 5) {|*x| x}) } @@ -2239,13 +2485,13 @@ eom end def test_class_module_Object_ancestors - assert_separately([], <<-RUBY) + assert_ruby_status([], <<-RUBY) m = Module.new m::Bug18832 = 1 include m class Bug18832; end RUBY - assert_separately([], <<-RUBY) + assert_ruby_status([], <<-RUBY) m = Module.new m::Bug18832 = 1 include m diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 12ba1165ed..c3d9dcf56d 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -798,7 +798,7 @@ class TestThread < Test::Unit::TestCase def for_test_handle_interrupt_with_return Thread.handle_interrupt(Object => :never){ - Thread.current.raise RuntimeError.new("have to be rescured") + Thread.current.raise RuntimeError.new("have to be rescued") return } rescue @@ -815,7 +815,7 @@ class TestThread < Test::Unit::TestCase assert_nothing_raised do begin Thread.handle_interrupt(Object => :never){ - Thread.current.raise RuntimeError.new("have to be rescured") + Thread.current.raise RuntimeError.new("have to be rescued") break } rescue @@ -1480,6 +1480,8 @@ q.pop end def test_thread_interrupt_for_killed_thread + pend "hang-up" if /mswin|mingw/ =~ RUBY_PLATFORM + opts = { timeout: 5, timeout_error: nil } assert_normal_exit(<<-_end, '[Bug #8996]', **opts) @@ -1589,4 +1591,107 @@ q.pop frame_for_deadlock_test_2 { t.join } INPUT end + + def test_unlock_locked_mutex_with_collected_fiber + bug21342 = '[ruby-core:122121] [Bug #21342]' + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}", bug21342) + begin; + 5.times do + m = Mutex.new + Thread.new do + m.synchronize do + end + end.join + Fiber.new do + GC.start + m.lock + end.resume + end + end; + end + + def test_unlock_locked_mutex_with_collected_fiber2 + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + MUTEXES = [] + 5.times do + m = Mutex.new + Fiber.new do + GC.start + m.lock + end.resume + MUTEXES << m + end + 10.times do + MUTEXES.clear + GC.start + end + end; + end + + def test_mutexes_locked_in_fiber_dont_have_aba_issue_with_new_fibers + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + mutexes = 1000.times.map do + Mutex.new + end + + mutexes.map do |m| + Fiber.new do + m.lock + end.resume + end + + GC.start + + 1000.times.map do + Fiber.new do + raise "FAILED!" if mutexes.any?(&:owned?) + end.resume + end + end; + end + + # [Bug #21836] + def test_mn_threads_sub_millisecond_sleep + assert_separately([{'RUBY_MN_THREADS' => '1'}], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30) + begin; + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1000.times { sleep 0.0001 } + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + elapsed = t1 - t0 + assert_operator elapsed, :>=, 0.1, "sub-millisecond sleeps should not return immediately" + end; + end + + # [Bug #21926] + def test_thread_join_during_finalizers + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) + begin; + require 'open3' + + class ProcessWrapper + def initialize + @stdin, @stdout, @stderr, @wait_thread = Open3.popen3("cat") # hangs until we close our stdin side + ObjectSpace.define_finalizer(self, self.class.make_finalizer(@stdin, @stdout, @stderr, @wait_thread)) + end + + def self.make_finalizer(stdin, stdout, stderr, wait_thread) + proc do + stdin.close rescue nil + stdout.close rescue nil + stderr.close rescue nil + # On some GC implementations (e.g. mmtk), finalizers run as postponed + # jobs which can execute on any thread, including the wait_thread itself. + # Guard against joining the current thread. + wait_thread.value unless Thread.current == wait_thread + end + end + end + + 20.times { ProcessWrapper.new } + GC.stress = true + 1000.times { Object.new } + end; + end end diff --git a/test/ruby/test_thread_cv.rb b/test/ruby/test_thread_cv.rb index eb88b9606c..e5fd513c5c 100644 --- a/test/ruby/test_thread_cv.rb +++ b/test/ruby/test_thread_cv.rb @@ -70,13 +70,13 @@ class TestThreadConditionVariable < Test::Unit::TestCase end end end - sleep 0.1 + Thread.pass until threads.all?(&:stop?) mutex.synchronize do result << "P1" condvar.broadcast result << "P2" end - Timeout.timeout(5) do + Timeout.timeout(60) do nr_threads.times do |i| threads[i].join end diff --git a/test/ruby/test_thread_queue.rb b/test/ruby/test_thread_queue.rb index 9485528977..4046185fd2 100644 --- a/test/ruby/test_thread_queue.rb +++ b/test/ruby/test_thread_queue.rb @@ -217,7 +217,7 @@ class TestThreadQueue < Test::Unit::TestCase bug5343 = '[ruby-core:39634]' Dir.mktmpdir {|d| - timeout = 60 + timeout = 120 total_count = 250 begin assert_normal_exit(<<-"_eom", bug5343, timeout: timeout, chdir: d) @@ -379,7 +379,7 @@ class TestThreadQueue < Test::Unit::TestCase assert_equal false, q.closed? q << :something assert_equal q, q.close - assert q.closed? + assert_predicate q, :closed? assert_raise_with_message(ClosedQueueError, /closed/){q << :nothing} assert_equal q.pop, :something assert_nil q.pop @@ -433,7 +433,7 @@ class TestThreadQueue < Test::Unit::TestCase assert_equal 1, q.size assert_equal :one, q.pop - assert q.empty?, "queue not empty" + assert_empty q end # make sure that shutdown state is handled properly by empty? for the non-blocking case @@ -567,7 +567,7 @@ class TestThreadQueue < Test::Unit::TestCase assert_equal 0, q.size assert_equal 3, ary.size - ary.each{|e| assert [0,1,2,3,4,5].include?(e)} + ary.each{|e| assert_include [0,1,2,3,4,5], e} assert_nil q.pop prod_threads.each{|t| diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index 333edb8021..b2cbd06a9f 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -1421,7 +1421,7 @@ class TestTime < Test::Unit::TestCase # Time objects are common in some code, try to keep them small omit "Time object size test" if /^(?:i.?86|x86_64)-linux/ !~ RUBY_PLATFORM omit "GC is in debug" if GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] > 0 - omit "memsize is not accurate due to using malloc_usable_size" if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit "memsize is not accurate due to using malloc_usable_size" if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 omit "Only run this test on 64-bit" if RbConfig::SIZEOF["void*"] != 8 require 'objspace' @@ -1433,7 +1433,10 @@ class TestTime < Test::Unit::TestCase RbConfig::SIZEOF["void*"] # Same size as VALUE end sizeof_vtm = RbConfig::SIZEOF["void*"] * 4 + 8 - expect = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + sizeof_timew + sizeof_vtm + data_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + sizeof_timew + sizeof_vtm + # Round up to the smallest slot size that fits + slot_sizes = GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times.map { |i| GC.stat_heap(i, :slot_size) } + expect = slot_sizes.find { |s| s >= data_size } || slot_sizes.last assert_operator ObjectSpace.memsize_of(t), :<=, expect rescue LoadError => e omit "failed to load objspace: #{e.message}" diff --git a/test/ruby/test_time_tz.rb b/test/ruby/test_time_tz.rb index f66cd9bec2..473c3cabcb 100644 --- a/test/ruby/test_time_tz.rb +++ b/test/ruby/test_time_tz.rb @@ -1,6 +1,5 @@ # frozen_string_literal: false require 'test/unit' -require '-test-/time' class TestTimeTZ < Test::Unit::TestCase has_right_tz = true diff --git a/test/ruby/test_transcode.rb b/test/ruby/test_transcode.rb index 71e1cc9e2a..2c4462eb71 100644 --- a/test/ruby/test_transcode.rb +++ b/test/ruby/test_transcode.rb @@ -2321,7 +2321,7 @@ class TestTranscode < Test::Unit::TestCase end def test_ractor_lazy_load_encoding - assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) begin; rs = [] autoload_encodings = Encoding.list.select { |e| e.inspect.include?("(autoload)") }.freeze @@ -2338,13 +2338,13 @@ class TestTranscode < Test::Unit::TestCase r, _obj = Ractor.select(*rs) rs.delete(r) end - assert rs.empty? + assert_empty rs end; end def test_ractor_lazy_load_encoding_random omit 'unstable on s390x and windows' if RUBY_PLATFORM =~ /s390x|mswin/ - assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30) begin; rs = [] 100.times do @@ -2357,7 +2357,7 @@ class TestTranscode < Test::Unit::TestCase r, _obj = Ractor.select(*rs) rs.delete(r) end - assert rs.empty? + assert_empty rs end; end @@ -2380,12 +2380,12 @@ class TestTranscode < Test::Unit::TestCase r, _obj = Ractor.select(*rs) rs.delete(r) end - assert rs.empty? + assert_empty rs end; end def test_ractor_asciicompat_encoding_doesnt_exist - assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) begin; rs = [] NO_EXIST = "I".freeze @@ -2403,7 +2403,7 @@ class TestTranscode < Test::Unit::TestCase r, _obj = Ractor.select(*rs) rs.delete(r) end - assert rs.empty? + assert_empty rs end; end diff --git a/test/ruby/test_variable.rb b/test/ruby/test_variable.rb index 3504b9b7dc..a305ad6b2a 100644 --- a/test/ruby/test_variable.rb +++ b/test/ruby/test_variable.rb @@ -50,6 +50,11 @@ class TestVariable < Test::Unit::TestCase end Zeus = Gods.clone + class Zeus + def ruler5 + @@rule + end + end def test_cloned_allows_setting_cvar Zeus.class_variable_set(:@@rule, "Athena") @@ -58,8 +63,12 @@ class TestVariable < Test::Unit::TestCase zeus = Zeus.new.ruler0 assert_equal "Cronus", god - assert_equal "Athena", zeus - assert_not_equal god.object_id, zeus.object_id + assert_equal "Cronus", zeus + + assert_equal "Athena", Zeus.new.ruler5 + + assert_equal "Cronus", Gods.class_variable_get(:@@rule) + assert_equal "Athena", Zeus.class_variable_get(:@@rule) end def test_singleton_class_included_class_variable @@ -120,6 +129,34 @@ class TestVariable < Test::Unit::TestCase TestVariable.send(:remove_const, :Parent) rescue nil end + def test_cvar_cache_invalidated_by_parent_class_variable_set + m = Module.new { class_variable_set(:@@x, 1) } + a = Class.new + b = Class.new(a) do + include m + class_eval "def self.x; @@x; end" + end + assert_equal 1, b.x # warm cache + a.class_variable_set(:@@x, 2) + error = assert_raise(RuntimeError) { b.x } + assert_match(/class variable @@x of .+ is overtaken by .+/, error.message) + end + + def test_cvar_cache_invalidated_by_module_class_variable_set + m = Module.new + n = Module.new + b = Class.new do + include m + include n + class_eval "def self.x; @@x; end" + end + m.class_variable_set(:@@x, 1) + assert_equal 1, b.x # warm cache + n.class_variable_set(:@@x, 2) + error = assert_raise(RuntimeError) { b.x } + assert_match(/class variable @@x of .+ is overtaken by .+/, error.message) + end + def test_cvar_overtaken_by_module error = eval <<~EORB class ParentForModule @@ -388,6 +425,61 @@ class TestVariable < Test::Unit::TestCase end end + class RemoveIvar + class << self + attr_reader :ivar + + def add_ivar + @ivar = 1 + end + end + + attr_reader :ivar + + def add_ivar + @ivar = 1 + end + end + + def add_and_remove_ivar(obj) + assert_nil obj.ivar + assert_equal 1, obj.add_ivar + assert_equal 1, obj.instance_variable_get(:@ivar) + assert_equal 1, obj.ivar + + obj.remove_instance_variable(:@ivar) + assert_nil obj.ivar + + assert_raise NameError do + obj.remove_instance_variable(:@ivar) + end + end + + def test_remove_instance_variables_object + obj = RemoveIvar.new + add_and_remove_ivar(obj) + add_and_remove_ivar(obj) + end + + def test_remove_instance_variables_class + add_and_remove_ivar(RemoveIvar) + add_and_remove_ivar(RemoveIvar) + end + + class RemoveIvarGeneric < Array + attr_reader :ivar + + def add_ivar + @ivar = 1 + end + end + + def test_remove_instance_variables_generic + obj = RemoveIvarGeneric.new + add_and_remove_ivar(obj) + add_and_remove_ivar(obj) + end + class ExIvar < Hash def initialize @a = 1 @@ -457,7 +549,37 @@ class TestVariable < Test::Unit::TestCase instance.instance_variable_set(:@a3, 3) instance.instance_variable_set(:@a4, 4) end.resume - assert_equal 4, instance.instance_variable_get(:@a4) + assert_equal 4, instance.instance_variable_get(:@a4), bug21547 + end + + def test_genivar_cache_free + str = +"hello" + str.instance_variable_set(:@x, :old_value) + + str.instance_variable_get(:@x) # populate cache + + Fiber.new { + str.remove_instance_variable(:@x) + str.instance_variable_set(:@x, :new_value) + }.resume + + assert_equal :new_value, str.instance_variable_get(:@x) + end + + def test_genivar_cache_invalidated_by_gc + str = +"hello" + str.instance_variable_set(:@x, :old_value) + + str.instance_variable_get(:@x) # populate cache + + Fiber.new { + str.remove_instance_variable(:@x) + str.instance_variable_set(:@x, :new_value) + }.resume + + GC.start + + assert_equal :new_value, str.instance_variable_get(:@x) end private diff --git a/test/ruby/test_vm_dump.rb b/test/ruby/test_vm_dump.rb index a3e7b69913..d183e03391 100644 --- a/test/ruby/test_vm_dump.rb +++ b/test/ruby/test_vm_dump.rb @@ -5,6 +5,7 @@ return unless /darwin/ =~ RUBY_PLATFORM class TestVMDump < Test::Unit::TestCase def assert_darwin_vm_dump_works(args, timeout=nil) + args.unshift({"RUBY_ON_BUG" => nil, "RUBY_CRASH_REPORT" => nil}) assert_in_out_err(args, "", [], /^\[IMPORTANT\]/, timeout: timeout || 300) end @@ -13,7 +14,7 @@ class TestVMDump < Test::Unit::TestCase end def test_darwin_segv_in_syscall - assert_darwin_vm_dump_works('-e1.times{Process.kill :SEGV,$$}') + assert_darwin_vm_dump_works(['-e1.times{Process.kill :SEGV,$$}']) end def test_darwin_invalid_access diff --git a/test/ruby/test_weakmap.rb b/test/ruby/test_weakmap.rb index a2904776bc..2f5c747339 100644 --- a/test/ruby/test_weakmap.rb +++ b/test/ruby/test_weakmap.rb @@ -39,6 +39,13 @@ class TestWeakMap < Test::Unit::TestCase assert_same(:foo, @wm[x]) end + def test_aset_returns_value + key = Object.new + value = Object.new + + assert_same(value, @wm.send(:[]=, key, value)) + end + def assert_weak_include(m, k, n = 100) if n > 0 return assert_weak_include(m, k, n-1) @@ -203,7 +210,7 @@ class TestWeakMap < Test::Unit::TestCase @wm[i] = obj end - assert_separately([], <<-'end;') + assert_ruby_status([], <<-'end;') wm = ObjectSpace::WeakMap.new obj = Object.new 100.times do @@ -224,7 +231,7 @@ class TestWeakMap < Test::Unit::TestCase assert_equal(val, wm[key]) end; - assert_separately(["-W0"], <<-'end;') + assert_ruby_status(["-W0"], <<-'end;') wm = ObjectSpace::WeakMap.new ary = 10_000.times.map do @@ -265,4 +272,27 @@ class TestWeakMap < Test::Unit::TestCase 10_000.times { weakmap[Object.new] = Object.new } RUBY end + + def test_generational_gc + EnvUtil.without_gc do + wmap = ObjectSpace::WeakMap.new + + (GC::INTERNAL_CONSTANTS[:RVALUE_OLD_AGE] - 1).times { GC.start } + + retain = [] + 50.times do + k = Object.new + wmap[k] = true + retain << k + end + + GC.start # WeakMap promoted, other objects still young + + retain.clear + + GC.start(full_mark: false) + + wmap.keys.each(&:itself) # call method on keys to cause crash + end + end end diff --git a/test/ruby/test_yield.rb b/test/ruby/test_yield.rb index 9b2b2f37e0..e7e65fce9e 100644 --- a/test/ruby/test_yield.rb +++ b/test/ruby/test_yield.rb @@ -401,7 +401,7 @@ class TestRubyYieldGen < Test::Unit::TestCase def test_block_cached_argc # [Bug #11451] - assert_separately([], <<-"end;") + assert_ruby_status([], <<-"end;") class Yielder def each yield :x, :y, :z diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 25399d1e62..0d7fe66e1c 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -133,7 +133,7 @@ class TestYJIT < Test::Unit::TestCase end def test_yjit_enable_with_monkey_patch - assert_separately(%w[--yjit-disable], <<~RUBY) + assert_ruby_status(%w[--yjit-disable], <<~RUBY) # This lets rb_method_entry_at(rb_mKernel, ...) return NULL Kernel.prepend(Module.new) @@ -547,7 +547,7 @@ class TestYJIT < Test::Unit::TestCase end def test_opt_getconstant_path_slowpath - assert_compiles(<<~RUBY, exits: { opt_getconstant_path: 1 }, result: [42, 42, 1, 1], call_threshold: 2) + assert_compiles(<<~RUBY, result: [42, 42, 1, 1], call_threshold: 2) class A FOO = 42 class << self @@ -644,6 +644,40 @@ class TestYJIT < Test::Unit::TestCase RUBY end + STRUCT_MAX_EMBEDDED_MEMBERS = ( + GC::INTERNAL_CONSTANTS[:RVARGC_MAX_ALLOCATE_SIZE] - + GC::INTERNAL_CONSTANTS[:RBASIC_SIZE] - + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] + ) / RbConfig::SIZEOF["void*"] + + def test_spilled_struct_aref + omit("FIXME: https://github.com/Shopify/ruby/issues/977") + assert_compiles(<<~RUBY) + LargeStruct = Struct.new(:foo, :bar, *(#{STRUCT_MAX_EMBEDDED_MEMBERS} - 2).times.map { :"m_\#{it}" }) + + def foo(obj) + foo = obj.foo + raise "Expected 1, got: \#{foo}" unless foo == 1 + bar = obj.bar + raise "Expected 2, got: \#{bar}" unless bar == 2 + end + + embedded_struct = LargeStruct.new(1, 2) + # Bump RCLASS_MAX_IV_COUNT for LargeStruct + embedded_struct.instance_variable_set(:@test, 1) + + # Next allocation reserves space for the imemo/fields reference. + heap_struct = LargeStruct.new(1, 2) + + RubyVM::YJIT.reset_stats! + + foo(embedded_struct) + foo(embedded_struct) + foo(heap_struct) + foo(heap_struct) + RUBY + end + def test_struct_aset assert_compiles(<<~RUBY) def foo(obj) @@ -657,6 +691,26 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_struct_aset_guards_recv_is_not_frozen + assert_compiles(<<~RUBY, result: :ok, exits: { opt_send_without_block: 1 }) + def foo(obj) + obj.foo = 123 + end + + Foo = Struct.new(:foo) + obj = Foo.new(123) + 100.times do + foo(obj) + end + obj.freeze + begin + foo(obj) + rescue FrozenError + :ok + end + RUBY + end + def test_getblockparam assert_compiles(<<~'RUBY', insns: [:getblockparam]) def foo &blk @@ -953,6 +1007,40 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_super_bmethod + # Bmethod defined at class scope + assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: true, exits: {}) + class SuperItself + define_method(:itself) { super() } + end + + obj = SuperItself.new + obj.itself + obj.itself == obj + RUBY + + # Bmethod defined inside a method (the block's local_iseq is ISEQ_TYPE_METHOD + # but the CME is at the bmethod frame, not the enclosing method's frame) + assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: "Base#foo via bmethod", exits: {}) + class Base + def foo = "Base#foo" + end + + class SetupHelper + def add_bmethod_to(klass) + klass.define_method(:foo) { super() + " via bmethod" } + end + end + + class Target < Base; end + + SetupHelper.new.add_bmethod_to(Target) + obj = Target.new + obj.foo + obj.foo + RUBY + end + # Tests calling a variadic cfunc with many args def test_build_large_struct assert_compiles(<<~RUBY, insns: %i[opt_send_without_block], call_threshold: 2) @@ -1534,14 +1622,6 @@ class TestYJIT < Test::Unit::TestCase RUBY end - def test_opt_aref_with - assert_compiles(<<~RUBY, insns: %i[opt_aref_with], result: "bar", frozen_string_literal: false) - h = {"foo" => "bar"} - - h["foo"] - RUBY - end - def test_proc_block_arg assert_compiles(<<~RUBY, result: [:proc, :no_block]) def yield_if_given = block_given? ? yield : :no_block @@ -1780,6 +1860,62 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_proc_block_with_kwrest + # When the bug was present this required --yjit-stats to trigger. + assert_compiles(<<~RUBY, result: {extra: 5}) + def foo = bar(w: 1, x: 2, y: 3, z: 4, extra: 5, &proc { _1 }) + def bar(w:, x:, y:, z:, **kwrest) = yield kwrest + + GC.stress = true + foo + foo + RUBY + end + + def test_yjit_dump_insns + # Testing that this undocumented debugging feature doesn't crash + args = [ + '--yjit-call-threshold=1', + '--yjit-dump-insns', + '-e def foo(case:) = {case:}[:case]', + '-e foo(case:0)', + ] + _out, _err, status = invoke_ruby(args, '', true, true) + assert_not_predicate(status, :signaled?) + end + + def test_yjit_prelude_kernel_prepend + # Simulate what bundler/setup can do: prepend a module to Kernel during + # the prelude via the BUNDLER_SETUP mechanism in rubygems.rb: + # require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler) + Tempfile.create(["kernel_prepend", ".rb"]) do |f| + f.write("Kernel.prepend(Module.new)\n") + f.flush + assert_separately([{ "BUNDLER_SETUP" => f.path }, "--enable=gems", "--yjit"], "", ignore_stderr: true) + end + end + + def test_exceptional_entry_into_env_escaped_before_yjit_enablement + threshold = 2 + assert_separately(["--disable-all", "--yjit-disable", "--yjit-call-threshold=#{threshold}"], <<~RUBY) + def run + @captured_env = ->{} + RubyVM::YJIT.enable + + i = 0 + while i < #{threshold} + next_i = i + 1 + from_break = tap { break i + 1 } # break from the block generates an exceptional entry + assert_equal(from_break, next_i, '[Bug #21941]') + i = next_i + end + end + + run + assert_equal(#{threshold}, @captured_env.binding.local_variable_get(:i)) + RUBY + end + private def code_gc_helpers diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 8f83e858a6..a56fea6d51 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -27,206 +27,125 @@ class TestZJIT < Test::Unit::TestCase RUBY end - def test_enable_through_env - child_env = {'RUBY_YJIT_ENABLE' => nil, 'RUBY_ZJIT_ENABLE' => '1'} - assert_in_out_err([child_env, '-v'], '') do |stdout, stderr| - assert_includes(stdout.first, '+ZJIT') - assert_equal([], stderr) - end - end - - def test_call_itself - assert_compiles '42', <<~RUBY, call_threshold: 2 - def test = 42.itself - test - test + def test_stats_string_no_zjit + assert_runs 'nil', <<~RUBY, zjit: false + RubyVM::ZJIT.stats_string + RUBY + assert_runs 'true', <<~RUBY, stats: false + RubyVM::ZJIT.stats_string.is_a?(String) + RUBY + assert_runs 'true', <<~RUBY, stats: true + RubyVM::ZJIT.stats_string.is_a?(String) RUBY end - def test_nil - assert_compiles 'nil', %q{ - def test = nil - test - } - end - - def test_putobject - assert_compiles '1', %q{ - def test = 1 - test - } - end - - def test_putstring - assert_compiles '""', %q{ - def test = "#{""}" - test - }, insns: [:putstring] - end - - def test_putchilldedstring - assert_compiles '""', %q{ - def test = "" + def test_stats_quiet + # Test that --zjit-stats-quiet collects stats but doesn't print them + script = <<~RUBY + def test = 42 test - }, insns: [:putchilledstring] - end - - def test_leave_param - assert_compiles '5', %q{ - def test(n) = n - test(5) - } - end - - def test_setglobal - assert_compiles '1', %q{ - def test - $a = 1 - $a - end - test - }, insns: [:setglobal] - end + puts RubyVM::ZJIT.stats_enabled? + RUBY - def test_string_intern - assert_compiles ':foo123', %q{ - def test - :"foo#{123}" - end + stats_header = "***ZJIT: Printing ZJIT statistics on exit***" - test - }, insns: [:intern] - end + # With --zjit-stats, stats should be printed to stderr + out, err, status = eval_with_jit(script, stats: true) + assert_success(out, err, status) + assert_includes(err, stats_header) + assert_equal("true\n", out) - def test_setglobal_with_trace_var_exception - assert_compiles '"rescued"', %q{ - def test - $a = 1 - rescue - "rescued" - end - - trace_var(:$a) { raise } - test - }, insns: [:setglobal] - end + # With --zjit-stats-quiet, stats should NOT be printed but still enabled + out, err, status = eval_with_jit(script, stats: :quiet) + assert_success(out, err, status) + refute_includes(err, stats_header) + assert_equal("true\n", out) - def test_setlocal - assert_compiles '3', %q{ - def test(n) - m = n - m - end - test(3) - } - end + # With --zjit-stats=<path>, stats should be printed to the path + Tempfile.create("zjit-stats-") {|tmp| + stats_file = tmp.path + tmp.puts("Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...") + tmp.close - def test_setlocal_on_eval - assert_compiles '1', %q{ - @b = binding - eval('a = 1', @b) - eval('a', @b) + out, err, status = eval_with_jit(script, stats: stats_file) + assert_success(out, err, status) + refute_includes(err, stats_header) + assert_equal("true\n", out) + assert_equal stats_header, File.open(stats_file) {|f| f.gets(chomp: true)}, "should be overwritten" } end - def test_call_a_forwardable_method - assert_runs '[]', %q{ - def test_root = forwardable - def forwardable(...) = Array.[](...) - test_root - test_root - }, call_threshold: 2 + def test_enable_through_env + child_env = {'RUBY_YJIT_ENABLE' => nil, 'RUBY_ZJIT_ENABLE' => '1'} + assert_in_out_err([child_env, '-v'], '') do |stdout, stderr| + assert_includes(stdout.first, '+ZJIT') + assert_equal([], stderr) + end end - def test_setlocal_on_eval_with_spill - assert_compiles '1', %q{ - @b = binding - eval('a = 1; itself', @b) - eval('a', @b) - } - end + def test_zjit_enable + # --disable-all is important in case the build/environment has YJIT enabled by + # default through e.g. -DYJIT_FORCE_ENABLE. Can't enable ZJIT when YJIT is on. + assert_separately(["--disable-all"], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" - def test_nested_local_access - assert_compiles '[1, 2, 3]', %q{ - 1.times do |l2| - 1.times do |l1| - define_method(:test) do - l1 = 1 - l2 = 2 - l3 = 3 - [l1, l2, l3] - end - end - end + RubyVM::ZJIT.enable - test - test - test - }, call_threshold: 3, insns: [:getlocal, :setlocal, :getlocal_WC_0, :setlocal_WC_1] + assert_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY end - def test_read_local_written_by_children_iseqs - omit "This test fails right now because Send doesn't compile." - - assert_compiles '[1, 2]', %q{ - def test - l1 = nil - l2 = nil - tap do |_| - l1 = 1 - tap do |_| - l2 = 2 - end - end + def test_zjit_disable + assert_separately(["--zjit", "--zjit-disable"], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" - [l1, l2] - end + RubyVM::ZJIT.enable - test - test - }, call_threshold: 2 + assert_predicate RubyVM::ZJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY end - def test_send_without_block - assert_compiles '[1, 2, 3]', %q{ - def foo = 1 - def bar(a) = a - 1 - def baz(a, b) = a - b - - def test1 = foo - def test2 = bar(3) - def test3 = baz(4, 1) - - [test1, test2, test3] - } + def test_zjit_prelude_kernel_prepend + # Simulate what bundler/setup can do: prepend a module to Kernel during + # the prelude via the BUNDLER_SETUP mechanism in rubygems.rb: + # require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler) + Tempfile.create(["kernel_prepend", ".rb"]) do |f| + f.write("Kernel.prepend(Module.new)\n") + f.flush + assert_separately([{ "BUNDLER_SETUP" => f.path }, "--enable=gems", "--zjit"], "", ignore_stderr: true) + end end - def test_send_with_six_args - assert_compiles '[1, 2, 3, 4, 5, 6]', %q{ - def foo(a1, a2, a3, a4, a5, a6) - [a1, a2, a3, a4, a5, a6] - end + def test_zjit_enable_respects_existing_options + assert_separately(['--zjit-disable', '--zjit-stats-quiet'], <<~RUBY) + refute_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? - def test - foo(1, 2, 3, 4, 5, 6) - end + RubyVM::ZJIT.enable - test # profile send - test - }, call_threshold: 2 + assert_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? + RUBY end - def test_send_on_heap_object_in_spilled_arg - # This leads to a register spill, so not using `assert_compiles` - assert_runs 'Hash', %q{ - def entry(a1, a2, a3, a4, a5, a6, a7, a8, a9) - a9.itself.class - end - - entry(1, 2, 3, 4, 5, 6, 7, 8, {}) # profile - entry(1, 2, 3, 4, 5, 6, 7, 8, {}) - }, call_threshold: 2 + def test_toplevel_binding + # Not using assert_compiles, which doesn't use the toplevel frame for `test_script`. + out, err, status = eval_with_jit(%q{ + a = 1 + b = 2 + TOPLEVEL_BINDING.local_variable_set(:b, 3) + c = 4 + print [a, b, c] + }) + assert_success(out, err, status) + assert_equal "[1, 3, 4]", out end def test_send_exit_with_uninitialized_locals @@ -245,889 +164,27 @@ class TestZJIT < Test::Unit::TestCase }, call_threshold: 2, allowed_iseqs: 'entry@-e:2' end - def test_invokebuiltin - omit 'Test fails at the moment due to not handling optional parameters' - assert_compiles '["."]', %q{ - def test = Dir.glob(".") - test - } - end - - def test_invokebuiltin_delegate - assert_compiles '[[], true]', %q{ - def test = [].clone(freeze: true) - r = test - [r, r.frozen?] - } - end - - def test_opt_plus_const - assert_compiles '3', %q{ - def test = 1 + 2 - test # profile opt_plus - test - }, call_threshold: 2 - end - - def test_opt_plus_fixnum - assert_compiles '3', %q{ - def test(a, b) = a + b - test(0, 1) # profile opt_plus - test(1, 2) - }, call_threshold: 2 - end - - def test_opt_plus_chain - assert_compiles '6', %q{ - def test(a, b, c) = a + b + c - test(0, 1, 2) # profile opt_plus - test(1, 2, 3) - }, call_threshold: 2 - end - - def test_opt_plus_left_imm - assert_compiles '3', %q{ - def test(a) = 1 + a - test(1) # profile opt_plus - test(2) - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_exit - assert_compiles '[3, 3.0]', %q{ - def test(a) = 1 + a - test(1) # profile opt_plus - [test(2), test(2.0)] - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_exit_with_locals - assert_compiles '[6, 6.0]', %q{ - def test(a) - local = 3 - 1 + a + local - end - test(1) # profile opt_plus - [test(2), test(2.0)] - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_nested_exit - assert_compiles '[4, 4.0]', %q{ - def side_exit(n) = 1 + n - def jit_frame(n) = 1 + side_exit(n) - def entry(n) = jit_frame(n) - entry(2) # profile send - [entry(2), entry(2.0)] - }, call_threshold: 2 - end - - def test_opt_plus_type_guard_nested_exit_with_locals - assert_compiles '[9, 9.0]', %q{ - def side_exit(n) - local = 2 - 1 + n + local - end - def jit_frame(n) - local = 3 - 1 + side_exit(n) + local - end - def entry(n) = jit_frame(n) - entry(2) # profile send - [entry(2), entry(2.0)] - }, call_threshold: 2 - end - - # Test argument ordering - def test_opt_minus - assert_compiles '2', %q{ - def test(a, b) = a - b - test(2, 1) # profile opt_minus - test(6, 4) - }, call_threshold: 2 - end - - def test_opt_mult - assert_compiles '6', %q{ - def test(a, b) = a * b - test(1, 2) # profile opt_mult - test(2, 3) - }, call_threshold: 2 - end - - def test_opt_mult_overflow - assert_compiles '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{ - def test(a, b) - a * b - end - test(1, 1) # profile opt_mult - - r1 = test(2, 3) - r2 = test(2, -3) - r3 = test(2 << 40, 2 << 41) - r4 = test(2 << 40, -2 << 41) - r5 = test(1 << 62, 1 << 62) - - [r1, r2, r3, r4, r5] - }, call_threshold: 2 - end - - def test_opt_eq - assert_compiles '[true, false]', %q{ - def test(a, b) = a == b - test(0, 2) # profile opt_eq - [test(1, 1), test(0, 1)] - }, insns: [:opt_eq], call_threshold: 2 - end - - def test_opt_eq_with_minus_one - assert_compiles '[false, true]', %q{ - def test(a) = a == -1 - test(1) # profile opt_eq - [test(0), test(-1)] - }, insns: [:opt_eq], call_threshold: 2 - end - - def test_opt_neq_dynamic - # TODO(max): Don't split this test; instead, run all tests with and without - # profiling. - assert_compiles '[false, true]', %q{ - def test(a, b) = a != b - test(0, 2) # profile opt_neq - [test(1, 1), test(0, 1)] - }, insns: [:opt_neq], call_threshold: 1 - end - - def test_opt_neq_fixnum - assert_compiles '[false, true]', %q{ - def test(a, b) = a != b - test(0, 2) # profile opt_neq - [test(1, 1), test(0, 1)] - }, call_threshold: 2 - end - - def test_opt_lt - assert_compiles '[true, false, false]', %q{ - def test(a, b) = a < b - test(2, 3) # profile opt_lt - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_lt], call_threshold: 2 - end - - def test_opt_lt_with_literal_lhs - assert_compiles '[false, false, true]', %q{ - def test(n) = 2 < n - test(2) # profile opt_lt - [test(1), test(2), test(3)] - }, insns: [:opt_lt], call_threshold: 2 - end - - def test_opt_le - assert_compiles '[true, true, false]', %q{ - def test(a, b) = a <= b - test(2, 3) # profile opt_le - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_le], call_threshold: 2 - end - - def test_opt_gt - assert_compiles '[false, false, true]', %q{ - def test(a, b) = a > b - test(2, 3) # profile opt_gt - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_gt], call_threshold: 2 - end - - def test_opt_empty_p - assert_compiles('[false, false, true]', <<~RUBY, insns: [:opt_empty_p]) - def test(x) = x.empty? - return test([1]), test("1"), test({}) - RUBY - end - - def test_opt_succ - assert_compiles('[0, "B"]', <<~RUBY, insns: [:opt_succ]) - def test(obj) = obj.succ - return test(-1), test("A") - RUBY - end - - def test_opt_and - assert_compiles('[1, [3, 2, 1]]', <<~RUBY, insns: [:opt_and]) - def test(x, y) = x & y - return test(0b1101, 3), test([3, 2, 1, 4], [8, 1, 2, 3]) - RUBY - end - - def test_opt_or - assert_compiles('[11, [3, 2, 1]]', <<~RUBY, insns: [:opt_or]) - def test(x, y) = x | y - return test(0b1000, 3), test([3, 2, 1], [1, 2, 3]) - RUBY - end - - def test_fixnum_and - assert_compiles '1', %q{ - def test(a, b) = a & b - test(2, 2) - test(2, 2) - test(5, 3) - }, call_threshold: 2, insns: [:opt_and] - end - - def test_fixnum_and_side_exit - assert_compiles 'false', %q{ - def test(a, b) = a & b - test(2, 2) - test(2, 2) - test(true, false) - }, call_threshold: 2, insns: [:opt_and] - end - - def test_fixnum_or - assert_compiles '3', %q{ - def test(a, b) = a | b - test(5, 3) - test(5, 3) - test(1, 2) - }, call_threshold: 2, insns: [:opt_or] - end - - def test_fixnum_or_side_exit - assert_compiles 'true', %q{ - def test(a, b) = a | b - test(2, 2) - test(2, 2) - test(true, false) - }, call_threshold: 2, insns: [:opt_or] - end - - def test_fixnum_mul - assert_compiles '12', %q{ - C = 3 - def test(n) = C * n - test(4) - test(4) - test(4) - }, call_threshold: 2, insns: [:opt_mult] - end - - def test_opt_not - assert_compiles('[true, true, false]', <<~RUBY, insns: [:opt_not]) - def test(obj) = !obj - return test(nil), test(false), test(0) - RUBY - end - - def test_opt_regexpmatch2 - assert_compiles('[1, nil]', <<~RUBY, insns: [:opt_regexpmatch2]) - def test(haystack) = /needle/ =~ haystack - return test("kneedle"), test("") - RUBY - end - - def test_opt_ge - assert_compiles '[false, true, true]', %q{ - def test(a, b) = a >= b - test(2, 3) # profile opt_ge - [test(0, 1), test(0, 0), test(1, 0)] - }, insns: [:opt_ge], call_threshold: 2 - end - - def test_new_hash_empty - assert_compiles '{}', %q{ - def test = {} - test - }, insns: [:newhash] - end - - def test_new_hash_nonempty - assert_compiles '{"key" => "value", 42 => 100}', %q{ - def test - key = "key" - value = "value" - num = 42 - result = 100 - {key => value, num => result} - end - test - }, insns: [:newhash] - end - - def test_new_hash_single_key_value - assert_compiles '{"key" => "value"}', %q{ - def test = {"key" => "value"} - test - }, insns: [:newhash] - end - - def test_new_hash_with_computation - assert_compiles '{"sum" => 5, "product" => 6}', %q{ - def test(a, b) - {"sum" => a + b, "product" => a * b} - end - test(2, 3) - }, insns: [:newhash] - end - - def test_new_hash_with_user_defined_hash_method - assert_runs 'true', %q{ - class CustomKey - attr_reader :val - - def initialize(val) - @val = val - end - - def hash - @val.hash - end - - def eql?(other) - other.is_a?(CustomKey) && @val == other.val - end - end - - def test - key = CustomKey.new("key") - hash = {key => "value"} - hash[key] == "value" - end - test - } + def test_opt_new_with_custom_allocator + assert_compiles '"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"', %q{ + require "digest" + def test = Digest::SHA256.new.hexdigest + test; test + }, insns: [:opt_new], call_threshold: 2 end - def test_new_hash_with_user_hash_method_exception - assert_runs 'RuntimeError', %q{ - class BadKey - def hash - raise "Hash method failed!" - end - end - + def test_opt_new_with_custom_allocator_raises + assert_compiles '[42, 42]', %q{ + require "digest" + class C < Digest::Base; end def test - key = BadKey.new - {key => "value"} - end - - begin - test - rescue => e - e.class - end - } - end - - def test_new_hash_with_user_eql_method_exception - assert_runs 'RuntimeError', %q{ - class BadKey - def hash + begin + Digest::Base.new + rescue NotImplementedError 42 end - - def eql?(other) - raise "Eql method failed!" - end - end - - def test - key1 = BadKey.new - key2 = BadKey.new - {key1 => "value1", key2 => "value2"} - end - - begin - test - rescue => e - e.class - end - } - end - - def test_opt_hash_freeze - assert_compiles '{}', <<~RUBY, insns: [:opt_hash_freeze] - def test = {}.freeze - test - RUBY - end - - def test_opt_ary_freeze - assert_compiles '[]', <<~RUBY, insns: [:opt_ary_freeze] - def test = [].freeze - test - RUBY - end - - def test_opt_str_freeze - assert_compiles '""', <<~RUBY, insns: [:opt_str_freeze] - def test = "".freeze - test - RUBY - end - - def test_opt_str_uminus - assert_compiles '""', <<~RUBY, insns: [:opt_str_uminus] - def test = -"" - test - RUBY - end - - def test_new_array_empty - assert_compiles '[]', %q{ - def test = [] - test - }, insns: [:newarray] - end - - def test_new_array_nonempty - assert_compiles '[5]', %q{ - def a = 5 - def test = [a] - test - } - end - - def test_new_array_order - assert_compiles '[3, 2, 1]', %q{ - def a = 3 - def b = 2 - def c = 1 - def test = [a, b, c] - test - } - end - - def test_array_dup - assert_compiles '[1, 2, 3]', %q{ - def test = [1,2,3] - test - } - end - - def test_new_range_inclusive - assert_compiles '1..5', %q{ - def test(a, b) = a..b - test(1, 5) - } - end - - def test_new_range_exclusive - assert_compiles '1...5', %q{ - def test(a, b) = a...b - test(1, 5) - } - end - - def test_new_range_with_literal - assert_compiles '3..10', %q{ - def test(n) = n..10 - test(3) - } - end - - def test_if - assert_compiles '[0, nil]', %q{ - def test(n) - if n < 5 - 0 - end - end - [test(3), test(7)] - } - end - - def test_if_else - assert_compiles '[0, 1]', %q{ - def test(n) - if n < 5 - 0 - else - 1 - end - end - [test(3), test(7)] - } - end - - def test_if_else_params - assert_compiles '[1, 20]', %q{ - def test(n, a, b) - if n < 5 - a - else - b - end - end - [test(3, 1, 2), test(7, 10, 20)] - } - end - - def test_if_else_nested - assert_compiles '[3, 8, 9, 14]', %q{ - def test(a, b, c, d, e) - if 2 < a - if a < 4 - b - else - c - end - else - if a < 0 - d - else - e - end - end - end - [ - test(-1, 1, 2, 3, 4), - test( 0, 5, 6, 7, 8), - test( 3, 9, 10, 11, 12), - test( 5, 13, 14, 15, 16), - ] - } - end - - def test_if_else_chained - assert_compiles '[12, 11, 21]', %q{ - def test(a) - (if 2 < a then 1 else 2 end) + (if a < 4 then 10 else 20 end) - end - [test(0), test(3), test(5)] - } - end - - def test_if_elsif_else - assert_compiles '[0, 2, 1]', %q{ - def test(n) - if n < 5 - 0 - elsif 8 < n - 1 - else - 2 - end - end - [test(3), test(7), test(9)] - } - end - - def test_ternary_operator - assert_compiles '[1, 20]', %q{ - def test(n, a, b) - n < 5 ? a : b - end - [test(3, 1, 2), test(7, 10, 20)] - } - end - - def test_ternary_operator_nested - assert_compiles '[2, 21]', %q{ - def test(n, a, b) - (n < 5 ? a : b) + 1 - end - [test(3, 1, 2), test(7, 10, 20)] - } - end - - def test_while_loop - assert_compiles '10', %q{ - def test(n) - i = 0 - while i < n - i = i + 1 - end - i - end - test(10) - } - end - - def test_while_loop_chain - assert_compiles '[135, 270]', %q{ - def test(n) - i = 0 - while i < n - i = i + 1 - end - while i < n * 10 - i = i * 3 - end - i - end - [test(5), test(10)] - } - end - - def test_while_loop_nested - assert_compiles '[0, 4, 12]', %q{ - def test(n, m) - i = 0 - while i < n - j = 0 - while j < m - j += 2 - end - i += j - end - i - end - [test(0, 0), test(1, 3), test(10, 5)] - } - end - - def test_while_loop_if_else - assert_compiles '[9, -1]', %q{ - def test(n) - i = 0 - while i < n - if n >= 10 - return -1 - else - i = i + 1 - end - end - i - end - [test(9), test(10)] - } - end - - def test_if_while_loop - assert_compiles '[9, 12]', %q{ - def test(n) - i = 0 - if n < 10 - while i < n - i += 1 - end - else - while i < n - i += 3 - end - end - i - end - [test(9), test(10)] - } - end - - def test_live_reg_past_ccall - assert_compiles '2', %q{ - def callee = 1 - def test = callee + callee - test - } - end - - def test_method_call - assert_compiles '12', %q{ - def callee(a, b) - a - b - end - - def test - callee(4, 2) + 10 - end - - test # profile test - test - }, call_threshold: 2 - end - - def test_recursive_fact - assert_compiles '[1, 6, 720]', %q{ - def fact(n) - if n == 0 - return 1 - end - return n * fact(n-1) - end - [fact(0), fact(3), fact(6)] - } - end - - def test_profiled_fact - assert_compiles '[1, 6, 720]', %q{ - def fact(n) - if n == 0 - return 1 - end - return n * fact(n-1) - end - fact(1) # profile fact - [fact(0), fact(3), fact(6)] - }, call_threshold: 3, num_profiles: 2 - end - - def test_recursive_fib - assert_compiles '[0, 2, 3]', %q{ - def fib(n) - if n < 2 - return n - end - return fib(n-1) + fib(n-2) - end - [fib(0), fib(3), fib(4)] - } - end - - def test_profiled_fib - assert_compiles '[0, 2, 3]', %q{ - def fib(n) - if n < 2 - return n - end - return fib(n-1) + fib(n-2) - end - fib(3) # profile fib - [fib(0), fib(3), fib(4)] - }, call_threshold: 5, num_profiles: 3 - end - - def test_spilled_basic_block_args - assert_compiles '55', %q{ - def test(n1, n2) - n3 = 3 - n4 = 4 - n5 = 5 - n6 = 6 - n7 = 7 - n8 = 8 - n9 = 9 - n10 = 10 - if n1 < n2 - n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8 + n9 + n10 - end - end - test(1, 2) - } - end - - def test_spilled_method_args - assert_runs '55', %q{ - def foo(n1, n2, n3, n4, n5, n6, n7, n8, n9, n10) - n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8 + n9 + n10 - end - - def test - foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - end - - test - } - - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - assert_runs '1', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8,n9) = n1+n9 - a(2,0,0,0,0,0,0,0,-1) - } - - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - assert_runs '0', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8) = n8 - a(1,1,1,1,1,1,1,0) - } - - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - # self param with spilled param - assert_runs '"main"', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8) = self - a(1,0,0,0,0,0,0,0).to_s - } - end - - def test_spilled_param_new_arary - # TODO(Shopify/ruby#716): Support spills and change to assert_compiles - assert_runs '[:ok]', %q{ - def a(n1,n2,n3,n4,n5,n6,n7,n8) = [n8] - a(0,0,0,0,0,0,0, :ok) - } - end - - def test_forty_param_method - # This used to a trigger a miscomp on A64 due - # to a memory displacement larger than 9 bits. - assert_compiles '1', %Q{ - def foo(#{'_,' * 39} n40) = n40 - - foo(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1) - } - end - - - def test_opt_aref_with - assert_compiles ':ok', %q{ - def aref_with(hash) = hash["key"] - - aref_with({ "key" => :ok }) - } - end - - def test_putself - assert_compiles '3', %q{ - class Integer - def minus(a) - self - a - end - end - 5.minus(2) - } - end - - def test_getinstancevariable - assert_compiles 'nil', %q{ - def test() = @foo - - test() - } - assert_compiles '3', %q{ - @foo = 3 - def test() = @foo - - test() - } - end - - def test_setinstancevariable - assert_compiles '1', %q{ - def test() = @foo = 1 - - test() - @foo - } - end - - def test_attr_reader - assert_compiles '[4, 4]', %q{ - class C - attr_reader :foo - - def initialize - @foo = 4 - end - end - - def test(c) = c.foo - c = C.new - [test(c), test(c)] - }, call_threshold: 2, insns: [:opt_send_without_block] - end - - def test_attr_accessor - assert_compiles '[4, 4]', %q{ - class C - attr_accessor :foo - - def initialize - @foo = 4 - end end - - def test(c) = c.foo - c = C.new - [test(c), test(c)] - }, call_threshold: 2, insns: [:opt_send_without_block] + [test, test] + }, insns: [:opt_new], call_threshold: 2 end def test_uncached_getconstant_path @@ -1151,78 +208,6 @@ class TestZJIT < Test::Unit::TestCase end end - def test_constant_invalidation - assert_compiles '123', <<~RUBY, call_threshold: 2, insns: [:opt_getconstant_path] - class C; end - def test = C - test - test - - C = 123 - test - RUBY - end - - def test_constant_path_invalidation - assert_compiles '["Foo::C", "Foo::C", "Bar::C"]', <<~RUBY, call_threshold: 2, insns: [:opt_getconstant_path] - module A - module B; end - end - - module Foo - C = "Foo::C" - end - - module Bar - C = "Bar::C" - end - - A::B = Foo - - def test = A::B::C - - result = [] - - result << test - result << test - - A::B = Bar - - result << test - result - RUBY - end - - def test_single_ractor_mode_invalidation - # Without invalidating the single-ractor mode, the test would crash - assert_compiles '"errored but not crashed"', <<~RUBY, call_threshold: 2, insns: [:opt_getconstant_path] - C = Object.new - - def test - C - rescue Ractor::IsolationError - "errored but not crashed" - end - - test - test - - Ractor.new { - test - }.value - RUBY - end - - def test_dupn - assert_compiles '[[1], [1, 1], :rhs, [nil, :rhs]]', <<~RUBY, insns: [:dupn] - def test(array) = (array[1, 2] ||= :rhs) - - one = [1, 1] - start_empty = [] - [test(one), one, test(start_empty), start_empty] - RUBY - end - def test_send_backtrace backtrace = [ "-e:2:in 'Object#jit_frame1'", @@ -1239,235 +224,6 @@ class TestZJIT < Test::Unit::TestCase }, call_threshold: 2 end - def test_bop_invalidation - omit 'Invalidation on BOP redefinition is not implemented yet' - assert_compiles '', %q{ - def test - eval(<<~RUBY) - class Integer - def +(_) = 100 - end - RUBY - 1 + 2 - end - test - } - end - - def test_defined_with_defined_values - assert_compiles '["constant", "method", "global-variable"]', %q{ - class Foo; end - def bar; end - $ruby = 1 - - def test = return defined?(Foo), defined?(bar), defined?($ruby) - - test - }, insns: [:defined] - end - - def test_defined_with_undefined_values - assert_compiles '[nil, nil, nil]', %q{ - def test = return defined?(Foo), defined?(bar), defined?($ruby) - - test - }, insns: [:defined] - end - - def test_defined_with_method_call - assert_compiles '["method", nil]', %q{ - def test = return defined?("x".reverse(1)), defined?("x".reverse(1).reverse) - - test - }, insns: [:defined] - end - - def test_defined_yield - assert_compiles "nil", "defined?(yield)" - assert_compiles '[nil, nil, "yield"]', %q{ - def test = defined?(yield) - [test, test, test{}] - }, call_threshold: 2, insns: [:defined] - end - - def test_defined_yield_from_block - # This will do some EP hopping to find the local EP, - # so it's slightly different than doing it outside of a block. - - omit 'Test fails at the moment due to missing Send codegen' - - assert_compiles '[nil, nil, "yield"]', %q{ - def test - yield_self { yield_self { defined?(yield) } } - end - - [test, test, test{}] - }, call_threshold: 2, insns: [:defined] - end - - def test_invokeblock_without_block_after_jit_call - assert_compiles '"no block given (yield)"', %q{ - def test(*arr, &b) - arr.class - yield - end - begin - test - rescue => e - e.message - end - } - end - - def test_putspecialobject_vm_core_and_cbase - assert_compiles '10', %q{ - def test - alias bar test - 10 - end - - test - bar - }, insns: [:putspecialobject] - end - - def test_putspecialobject_const_base - assert_compiles '1', %q{ - Foo = 1 - - def test = Foo - - # First call: populates the constant cache - test - # Second call: triggers ZJIT compilation with warm cache - # RubyVM::ZJIT.assert_compiles will panic if this fails to compile - test - }, call_threshold: 2 - end - - def test_branchnil - assert_compiles '[2, nil]', %q{ - def test(x) - x&.succ - end - [test(1), test(nil)] - }, call_threshold: 1, insns: [:branchnil] - end - - def test_nil_nil - assert_compiles 'true', %q{ - def test = nil.nil? - test - }, insns: [:opt_nil_p] - end - - def test_non_nil_nil - assert_compiles 'false', %q{ - def test = 1.nil? - test - }, insns: [:opt_nil_p] - end - - def test_getspecial_last_match - assert_compiles '"hello"', %q{ - def test(str) - str =~ /hello/ - $& - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_match_pre - assert_compiles '"hello "', %q{ - def test(str) - str =~ /world/ - $` - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_match_post - assert_compiles '" world"', %q{ - def test(str) - str =~ /hello/ - $' - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_match_last_group - assert_compiles '"world"', %q{ - def test(str) - str =~ /(hello) (world)/ - $+ - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_numbered_match_1 - assert_compiles '"hello"', %q{ - def test(str) - str =~ /(hello) (world)/ - $1 - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_numbered_match_2 - assert_compiles '"world"', %q{ - def test(str) - str =~ /(hello) (world)/ - $2 - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_numbered_match_nonexistent - assert_compiles 'nil', %q{ - def test(str) - str =~ /(hello)/ - $2 - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_no_match - assert_compiles 'nil', %q{ - def test(str) - str =~ /xyz/ - $& - end - test("hello world") - }, insns: [:getspecial] - end - - def test_getspecial_complex_pattern - assert_compiles '"123"', %q{ - def test(str) - str =~ /(\d+)/ - $1 - end - test("abc123def") - }, insns: [:getspecial] - end - - def test_getspecial_multiple_groups - assert_compiles '"456"', %q{ - def test(str) - str =~ /(\d+)-(\d+)/ - $2 - end - test("123-456") - }, insns: [:getspecial] - end - # tool/ruby_vm/views/*.erb relies on the zjit instructions a) being contiguous and # b) being reliably ordered after all the other instructions. def test_instruction_order @@ -1488,429 +244,190 @@ class TestZJIT < Test::Unit::TestCase end def test_require_rubygems_with_auto_compact + omit("GC.auto_compact= support is required for this test") unless GC.respond_to?(:auto_compact=) assert_runs 'true', %q{ GC.auto_compact = true require 'rubygems' }, call_threshold: 2 end - def test_stats + def test_stats_availability assert_runs '[true, true]', %q{ def test = 1 test [ - RubyVM::ZJIT.stats[:zjit_insns_count] > 0, - RubyVM::ZJIT.stats(:zjit_insns_count) > 0, + RubyVM::ZJIT.stats[:zjit_insn_count] > 0, + RubyVM::ZJIT.stats(:zjit_insn_count) > 0, ] }, stats: true end - def test_zjit_option_uses_array_each_in_ruby - omit 'ZJIT wrongly compiles Array#each, so it is disabled for now' - assert_runs '"<internal:array>"', %q{ - Array.instance_method(:each).source_location&.first - } - end - - def test_profile_under_nested_jit_call - assert_compiles '[nil, nil, 3]', %q{ - def profile - 1 + 2 - end + def test_stats_consistency + assert_runs '[]', %q{ + def test = 1 + test # increment some counters - def jit_call(flag) - if flag - profile + RubyVM::ZJIT.stats.to_a.filter_map do |key, value| + # The value may be incremented, but the class should stay the same + other_value = RubyVM::ZJIT.stats(key) + if value.class != other_value.class + [key, value, other_value] end end - - def entry(flag) - jit_call(flag) - end - - [entry(false), entry(false), entry(true)] - }, call_threshold: 2 - end - - def test_bop_redefinition - assert_runs '[3, :+, 100]', %q{ - def test - 1 + 2 - end - - test # profile opt_plus - [test, Integer.class_eval { def +(_) = 100 }, test] - }, call_threshold: 2 - end - - def test_bop_redefinition_with_adjacent_patch_points - assert_runs '[15, :+, 100]', %q{ - def test - 1 + 2 + 3 + 4 + 5 - end - - test # profile opt_plus - [test, Integer.class_eval { def +(_) = 100 }, test] - }, call_threshold: 2 + }, stats: true end - # ZJIT currently only generates a MethodRedefined patch point when the method - # is called on the top-level self. - def test_method_redefinition_with_top_self - assert_runs '["original", "redefined"]', %q{ - def foo - "original" - end - - def test = foo - - test; test - - result1 = test - - # Redefine the method - def foo - "redefined" - end + def test_reset_stats + assert_runs 'true', %q{ + def test = 1 + 100.times { test } - result2 = test + # Get initial stats and verify they're non-zero + initial_stats = RubyVM::ZJIT.stats - [result1, result2] - }, call_threshold: 2 - end + # Reset the stats + RubyVM::ZJIT.reset_stats! - def test_module_name_with_guard_passes - assert_compiles '"Integer"', %q{ - def test(mod) - mod.name - end + # Get stats after reset + reset_stats = RubyVM::ZJIT.stats - test(String) - test(Integer) - }, call_threshold: 2 + [ + # After reset, counters should be zero or at least much smaller + # (some instructions might execute between reset and reading stats) + :zjit_insn_count.then { |s| initial_stats[s] > 0 && reset_stats[s] < initial_stats[s] }, + :compiled_iseq_count.then { |s| initial_stats[s] > 0 && reset_stats[s] < initial_stats[s] } + ].all? + }, stats: true end - def test_module_name_with_guard_side_exit - # This test demonstrates that the guard side exit works correctly - # In this case, when we call with a non-Class object, it should fall back to interpreter - assert_compiles '["String", "Integer", "Bar"]', %q{ - class MyClass - def name = "Bar" - end - - def test(mod) - mod.name - end - - results = [] - results << test(String) - results << test(Integer) - results << test(MyClass.new) - - results - }, call_threshold: 2 + def test_zjit_option_uses_array_each_in_ruby + omit 'ZJIT wrongly compiles Array#each, so it is disabled for now' + assert_runs '"<internal:array>"', %q{ + Array.instance_method(:each).source_location&.first + } end - def test_objtostring_calls_to_s_on_non_strings - assert_compiles '["foo", "foo"]', %q{ - results = [] - - class Foo - def to_s - "foo" - end + def test_line_tracepoint_on_c_method + assert_compiles '"[[:line, true]]"', %q{ + events = [] + events.instance_variable_set( + :@tp, + TracePoint.new(:line) { |tp| events << [tp.event, tp.lineno] if tp.path == __FILE__ } + ) + def events.to_str + @tp.enable; '' end - def test(str) - "#{str}" + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + @tp.disable; __LINE__ end - results << test(Foo.new) - results << test(Foo.new) + line = events.compiled(events) + events[0][-1] = (events[0][-1] == line) - results + events.to_s # can't dump events as it's a singleton object AND it has a TracePoint instance variable, which also can't be dumped } end - def test_objtostring_rewrite_does_not_call_to_s_on_strings - assert_compiles '["foo", "foo"]', %q{ - results = [] - - class String - def to_s - "bad" - end + def test_targeted_line_tracepoint_in_c_method_call + assert_compiles '"[true]"', %q{ + events = [] + events.instance_variable_set(:@tp, TracePoint.new(:line) { |tp| events << tp.lineno }) + def events.to_str + @tp.enable(target: method(:compiled)) + '' end - def test(foo) - "#{foo}" + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + __LINE__ end - results << test("foo") - results << test("foo") + line = events.compiled(events) + events[0] = (events[0] == line) - results + events.to_s # can't dump events as it's a singleton object AND it has a TracePoint instance variable, which also can't be dumped } end - def test_objtostring_rewrite_does_not_call_to_s_on_string_subclasses - assert_compiles '["foo", "foo"]', %q{ - results = [] - - class StringSubclass < String - def to_s - "bad" - end + def test_regression_cfp_sp_set_correctly_before_leaf_gc_call + assert_compiles ':ok', %q{ + def check(l, r) + return 1 unless l + 1 + check(*l) + check(*r) end - foo = StringSubclass.new("foo") + def tree(depth) + # This duparray is our leaf-gc target. + return [nil, nil] unless depth > 0 - def test(str) - "#{str}" + # Modify the local and pass it to the following calls. + depth -= 1 + [tree(depth), tree(depth)] end - results << test(foo) - results << test(foo) - - results - } - end - - def test_string_bytesize_with_guard - assert_compiles '5', %q{ - def test(str) - str.bytesize + def test + GC.stress = true + 2.times do + t = tree(11) + check(*t) + end + :ok end - test('hello') - test('world') - }, call_threshold: 2 - end - - def test_nil_value_nil_opt_with_guard - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(nil) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_nil_value_nil_opt_with_guard_side_exit - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(nil) - test(nil) - test(1) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_true_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(true) - test(true) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_true_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(true) - test(true) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_false_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(false) - test(false) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_false_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(false) - test(false) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_integer_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(1) - test(2) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_integer_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(1) - test(2) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_float_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(1.0) - test(2.0) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_float_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(1.0) - test(2.0) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_symbol_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(:foo) - test(:bar) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_symbol_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(:foo) - test(:bar) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_class_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(String) - test(Integer) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_class_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(String) - test(Integer) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_module_nil_opt_with_guard - assert_compiles 'false', %q{ - def test(val) = val.nil? - - test(Enumerable) - test(Kernel) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_module_nil_opt_with_guard_side_exit - assert_compiles 'true', %q{ - def test(val) = val.nil? - - test(Enumerable) - test(Kernel) - test(nil) - }, call_threshold: 2, insns: [:opt_nil_p] - end - - def test_basic_object_guard_works_with_immediate - assert_compiles 'NilClass', %q{ - class Foo; end - - def test(val) = val.class - - test(Foo.new) - test(Foo.new) - test(nil) - }, call_threshold: 2 - end - - def test_basic_object_guard_works_with_false - assert_compiles 'FalseClass', %q{ - class Foo; end - - def test(val) = val.class - - test(Foo.new) - test(Foo.new) - test(false) - }, call_threshold: 2 - end - - def test_string_concat - assert_compiles '"123"', %q{ - def test = "#{1}#{2}#{3}" - test - }, insns: [:concatstrings] + }, call_threshold: 14, num_profiles: 5 end - def test_string_concat_empty - assert_compiles '""', %q{ - def test = "#{}" + def test_exit_tracing + # Smoke test: --zjit-trace-exits writes a Fuchsia trace (.fxt) file to /tmp + assert_compiles('true', <<~RUBY, extra_args: ['--zjit-trace-exits']) + def test(object) = object.itself - test - }, insns: [:concatstrings] - end - - def test_regexp_interpolation - assert_compiles '/123/', %q{ - def test = /#{1}#{2}#{3}/ + # induce an exit just for good measure + array = [] + test(array) + test(array) + def array.itself = :not_itself + test(array) - test - }, insns: [:toregexp] + fxt_files = Dir.glob("/tmp/perfetto-\#{Process.pid}.fxt") + result = fxt_files.length == 1 && !File.empty?(fxt_files.first) + File.unlink(*fxt_files) + result + RUBY end - def test_new_range_non_leaf - assert_compiles '(0/1)..1', %q{ - def jit_entry(v) = make_range_then_exit(v) - - def make_range_then_exit(v) - range = (v..1) - super rescue range # TODO(alan): replace super with side-exit intrinsic - end - - jit_entry(0) # profile - jit_entry(0) # compile - jit_entry(0/1r) # run without stub - }, call_threshold: 2 + def test_send_no_profiles_with_disabled_specialized_instruction + # Regression test: when specialized_instruction is disabled (as power_assert does), + # eval'd code uses `send` instead of `opt_send_without_block`, producing SendNoProfiles. + # The `times` call with a literal block is the SendNoProfiles send whose exit profiling + # triggers recompilation of `run`. After recompilation, `make`'s eval("proc { }") crashes + # in vm_make_env_each because the caller frame's EP[-1] (specval) has a stale value. + assert_runs ':ok', <<~RUBY + RubyVM::InstructionSequence.compile_option = { specialized_instruction: false } + eval <<~'INNERRUBY' + def make = eval("proc { }") + def run(n) = n.times { make } + INNERRUBY + run(6) + :ok + RUBY end - def test_raise_in_second_argument - assert_compiles '{ok: true}', %q{ - def write(hash, key) - hash[key] = raise rescue true - hash - end - - write({}, :ok) - } + def test_float_arithmetic + assert_compiles '4.0', 'def test = 1.5 + 2.5; test' + assert_compiles '6.0', 'def test = 2.0 * 3.0; test' + assert_compiles '1.5', 'def test = 3.5 - 2.0; test' + assert_compiles '2.5', 'def test = 5.0 / 2.0; test' + assert_compiles '4.5', 'def test = 1.5 * 3; test' # Float * Fixnum + assert_compiles 'true', 'def test = (Float::NAN + 1.0).nan?; test' + assert_compiles 'Infinity', 'def test = Float::INFINITY * 2.0; test' + assert_compiles '3', 'def test = 3.7.to_i; test' + assert_compiles '-2', 'def test = (-2.9).to_i; test' end private @@ -1926,31 +443,33 @@ class TestZJIT < Test::Unit::TestCase # allows ZJIT to skip compiling methods. def assert_runs(expected, test_script, insns: [], assert_compiles: false, **opts) pipe_fd = 3 + disasm_method = :test script = <<~RUBY ret_val = (_test_proc = -> { #{('RubyVM::ZJIT.assert_compiles; ' if assert_compiles)}#{test_script.lstrip} }).call result = { ret_val:, #{ unless insns.empty? - 'insns: RubyVM::InstructionSequence.of(method(:test)).to_a' + "insns: RubyVM::InstructionSequence.of(method(#{disasm_method.inspect})).to_a" end} } IO.open(#{pipe_fd}).write(Marshal.dump(result)) RUBY - status, out, err, result = eval_with_jit(script, pipe_fd:, **opts) - - message = "exited with status #{status.to_i}" - message << "\nstdout:\n```\n#{out}```\n" unless out.empty? - message << "\nstderr:\n```\n#{err}```\n" unless err.empty? - assert status.success?, message + out, err, status, result = eval_with_jit(script, pipe_fd:, **opts) + assert_success(out, err, status) result = Marshal.load(result) assert_equal(expected, result.fetch(:ret_val).inspect) unless insns.empty? iseq = result.fetch(:insns) - assert_equal("YARVInstructionSequence/SimpleDataFormat", iseq.first, "failed to get iseq disassembly") + assert_equal( + "YARVInstructionSequence/SimpleDataFormat", + iseq.first, + "Failed to get ISEQ disassembly. " \ + "Make sure to put code directly under the '#{disasm_method}' method." + ) iseq_insns = iseq.last expected_insns = Set.new(insns) @@ -1971,14 +490,22 @@ class TestZJIT < Test::Unit::TestCase stats: false, debug: true, allowed_iseqs: nil, + extra_args: nil, timeout: 1000, - pipe_fd: + pipe_fd: nil ) - args = ["--disable-gems"] + args = ["--disable-gems", *extra_args] if zjit args << "--zjit-call-threshold=#{call_threshold}" args << "--zjit-num-profiles=#{num_profiles}" - args << "--zjit-stats" if stats + case stats + when true + args << "--zjit-stats" + when :quiet + args << "--zjit-stats-quiet" + else + args << "--zjit-stats=#{stats}" if stats + end args << "--zjit-debug" if debug if allowed_iseqs jitlist = Tempfile.new("jitlist") @@ -1988,18 +515,25 @@ class TestZJIT < Test::Unit::TestCase end end args << "-e" << script_shell_encode(script) - pipe_r, pipe_w = IO.pipe - # Separate thread so we don't deadlock when - # the child ruby blocks writing the output to pipe_fd - pipe_out = nil - pipe_reader = Thread.new do - pipe_out = pipe_r.read - pipe_r.close + ios = {} + if pipe_fd + pipe_r, pipe_w = IO.pipe + # Separate thread so we don't deadlock when + # the child ruby blocks writing the output to pipe_fd + pipe_out = nil + pipe_reader = Thread.new do + pipe_out = pipe_r.read + pipe_r.close + end + ios[pipe_fd] = pipe_w end - out, err, status = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios: { pipe_fd => pipe_w }) - pipe_w.close - pipe_reader.join(timeout) - [status, out, err, pipe_out] + result = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios:) + if pipe_fd + pipe_w.close + pipe_reader.join(timeout) + result << pipe_out + end + result ensure pipe_reader&.kill pipe_reader&.join(timeout) @@ -2008,6 +542,13 @@ class TestZJIT < Test::Unit::TestCase jitlist&.unlink end + def assert_success(out, err, status) + message = "exited with status #{status.to_i}" + message << "\nstdout:\n```\n#{out}```\n" unless out.empty? + message << "\nstderr:\n```\n#{err}```\n" unless err.empty? + assert status.success?, message + end + def script_shell_encode(s) # We can't pass utf-8-encoded characters directly in a shell arg. But we can use Ruby \u constants. s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join |
