diff options
Diffstat (limited to 'test/ruby')
104 files changed, 9665 insertions, 969 deletions
diff --git a/test/ruby/box/a.1_1_0.rb b/test/ruby/box/a.1_1_0.rb new file mode 100644 index 0000000000..0322585097 --- /dev/null +++ b/test/ruby/box/a.1_1_0.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class BOX_A + VERSION = "1.1.0" + + def yay + "yay #{VERSION}" + end +end + +module BOX_B + VERSION = "1.1.0" + + def self.yay + "yay_b1" + end +end diff --git a/test/ruby/box/a.1_2_0.rb b/test/ruby/box/a.1_2_0.rb new file mode 100644 index 0000000000..29813ea57b --- /dev/null +++ b/test/ruby/box/a.1_2_0.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class BOX_A + VERSION = "1.2.0" + + def yay + "yay #{VERSION}" + end +end + +module BOX_B + VERSION = "1.2.0" + + def self.yay + "yay_b1" + end +end diff --git a/test/ruby/box/a.rb b/test/ruby/box/a.rb new file mode 100644 index 0000000000..26a622c92b --- /dev/null +++ b/test/ruby/box/a.rb @@ -0,0 +1,15 @@ +class BOX_A + FOO = "foo_a1" + + def yay + "yay_a1" + end +end + +module BOX_B + BAR = "bar_b1" + + def self.yay + "yay_b1" + end +end diff --git a/test/ruby/box/autoloading.rb b/test/ruby/box/autoloading.rb new file mode 100644 index 0000000000..cba57ab377 --- /dev/null +++ b/test/ruby/box/autoloading.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +autoload :BOX_A, File.join(__dir__, 'a.1_1_0') +BOX_A.new.yay + +module BOX_B + autoload :BAR, File.join(__dir__, 'a') +end diff --git a/test/ruby/box/blank.rb b/test/ruby/box/blank.rb new file mode 100644 index 0000000000..6d201b0966 --- /dev/null +++ b/test/ruby/box/blank.rb @@ -0,0 +1,2 @@ +module Blank1 +end diff --git a/test/ruby/box/blank1.rb b/test/ruby/box/blank1.rb new file mode 100644 index 0000000000..6d201b0966 --- /dev/null +++ b/test/ruby/box/blank1.rb @@ -0,0 +1,2 @@ +module Blank1 +end diff --git a/test/ruby/box/blank2.rb b/test/ruby/box/blank2.rb new file mode 100644 index 0000000000..ba38c1d6db --- /dev/null +++ b/test/ruby/box/blank2.rb @@ -0,0 +1,2 @@ +module Blank2 +end 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/box/call_proc.rb b/test/ruby/box/call_proc.rb new file mode 100644 index 0000000000..8acf538fc1 --- /dev/null +++ b/test/ruby/box/call_proc.rb @@ -0,0 +1,5 @@ +module Bar + def self.caller(proc_value) + proc_value.call + end +end diff --git a/test/ruby/box/call_toplevel.rb b/test/ruby/box/call_toplevel.rb new file mode 100644 index 0000000000..c311a37028 --- /dev/null +++ b/test/ruby/box/call_toplevel.rb @@ -0,0 +1,8 @@ +foo + +#### TODO: this code should be valid, but can't be for now +# module Foo +# def self.wow +# foo +# end +# end diff --git a/test/ruby/box/consts.rb b/test/ruby/box/consts.rb new file mode 100644 index 0000000000..e40cd5c50c --- /dev/null +++ b/test/ruby/box/consts.rb @@ -0,0 +1,148 @@ +$VERBOSE = nil +class String + STR_CONST1 = 111 + STR_CONST2 = 222 + STR_CONST3 = 333 +end + +class String + STR_CONST1 = 112 + + def self.set0(val) + const_set(:STR_CONST0, val) + end + + def self.remove0 + remove_const(:STR_CONST0) + end + + def refer0 + STR_CONST0 + end + + def refer1 + STR_CONST1 + end + + def refer2 + STR_CONST2 + end + + def refer3 + STR_CONST3 + end +end + +module ForConsts + CONST1 = 111 +end + +TOP_CONST = 10 + +module ForConsts + CONST1 = 112 + CONST2 = 222 + CONST3 = 333 + + def self.refer_all + ForConsts::CONST1 + ForConsts::CONST2 + ForConsts::CONST3 + String::STR_CONST1 + String::STR_CONST2 + String::STR_CONST3 + end + + def self.refer1 + CONST1 + end + + def self.get1 + const_get(:CONST1) + end + + def self.refer2 + CONST2 + end + + def self.get2 + const_get(:CONST2) + end + + def self.refer3 + CONST3 + end + + def self.get3 + const_get(:CONST3) + end + + def self.refer_top_const + TOP_CONST + end + + # for String + class Proxy + def call_str_refer0 + String.new.refer0 + end + + def call_str_get0 + String.const_get(:STR_CONST0) + end + + def call_str_set0(val) + String.set0(val) + end + + def call_str_remove0 + String.remove0 + end + + def call_str_refer1 + String.new.refer1 + end + + def call_str_get1 + String.const_get(:STR_CONST1) + end + + String::STR_CONST2 = 223 + + def call_str_refer2 + String.new.refer2 + end + + def call_str_get2 + String.const_get(:STR_CONST2) + end + + def call_str_set3 + String.const_set(:STR_CONST3, 334) + end + + def call_str_refer3 + String.new.refer3 + end + + def call_str_get3 + String.const_get(:STR_CONST3) + end + + # for Integer + Integer::INT_CONST1 = 1 + + def refer_int_const1 + Integer::INT_CONST1 + end + end +end + +# should not raise errors +ForConsts.refer_all +String::STR_CONST1 +Integer::INT_CONST1 + +# If we execute this sentence once, the constant value will be cached on ISeq inline constant cache. +# And it changes the behavior of ForConsts.refer_consts_directly called from global. +# ForConsts.refer_consts_directly # should not raise errors too diff --git a/test/ruby/box/define_toplevel.rb b/test/ruby/box/define_toplevel.rb new file mode 100644 index 0000000000..aa77db3a13 --- /dev/null +++ b/test/ruby/box/define_toplevel.rb @@ -0,0 +1,5 @@ +def foo + "foooooooooo" +end + +foo # should not raise errors diff --git a/test/ruby/box/global_vars.rb b/test/ruby/box/global_vars.rb new file mode 100644 index 0000000000..590363f617 --- /dev/null +++ b/test/ruby/box/global_vars.rb @@ -0,0 +1,37 @@ +module LineSplitter + def self.read + $-0 + end + + def self.write(char) + $-0 = char + end +end + +module FieldSplitter + def self.read + $, + end + + def self.write(char) + $, = char + end +end + +module UniqueGvar + def self.read + $used_only_in_box + end + + def self.write(val) + $used_only_in_box = val + end + + def self.write_only(val) + $write_only_var_in_box = val + end + + def self.gvars_in_box + global_variables + end +end diff --git a/test/ruby/box/instance_variables.rb b/test/ruby/box/instance_variables.rb new file mode 100644 index 0000000000..1562ad5d45 --- /dev/null +++ b/test/ruby/box/instance_variables.rb @@ -0,0 +1,21 @@ +class String + class << self + attr_reader :str_ivar1 + + def str_ivar2 + @str_ivar2 + end + end + + @str_ivar1 = 111 + @str_ivar2 = 222 +end + +class StringDelegator < BasicObject +private + def method_missing(...) + ::String.public_send(...) + end +end + +StringDelegatorObj = StringDelegator.new diff --git a/test/ruby/box/line_splitter.rb b/test/ruby/box/line_splitter.rb new file mode 100644 index 0000000000..2596975ad7 --- /dev/null +++ b/test/ruby/box/line_splitter.rb @@ -0,0 +1,9 @@ +module LineSplitter + def self.read + $-0 + end + + def self.write(char) + $-0 = char + end +end diff --git a/test/ruby/box/load_path.rb b/test/ruby/box/load_path.rb new file mode 100644 index 0000000000..7e5a83ef96 --- /dev/null +++ b/test/ruby/box/load_path.rb @@ -0,0 +1,26 @@ +module LoadPathCheck + FIRST_LOAD_PATH = $LOAD_PATH.dup + FIRST_LOAD_PATH_RESPOND_TO_RESOLVE = $LOAD_PATH.respond_to?(:resolve_feature_path) + FIRST_LOADED_FEATURES = $LOADED_FEATURES.dup + + HERE = File.dirname(__FILE__) + + def self.current_load_path + $LOAD_PATH + end + + def self.current_loaded_features + $LOADED_FEATURES + end + + def self.require_blank1 + $LOAD_PATH << HERE + require 'blank1' + end + + def self.require_blank2 + require 'blank2' + end +end + +LoadPathCheck.require_blank1 diff --git a/test/ruby/box/open_class_with_include.rb b/test/ruby/box/open_class_with_include.rb new file mode 100644 index 0000000000..ad8fd58ea0 --- /dev/null +++ b/test/ruby/box/open_class_with_include.rb @@ -0,0 +1,31 @@ +module StringExt + FOO = "foo 1" + def say_foo + "I'm saying " + FOO + end +end + +class String + include StringExt + def say + say_foo + end +end + +module OpenClassWithInclude + def self.say + String.new.say + end + + def self.say_foo + String.new.say_foo + end + + def self.say_with_obj(str) + str.say + end + + def self.refer_foo + String::FOO + end +end diff --git a/test/ruby/box/proc_callee.rb b/test/ruby/box/proc_callee.rb new file mode 100644 index 0000000000..d30ab5d9f3 --- /dev/null +++ b/test/ruby/box/proc_callee.rb @@ -0,0 +1,14 @@ +module Target + def self.foo + "fooooo" + end +end + +module Foo + def self.callee + lambda do + Target.foo + end + end +end + diff --git a/test/ruby/box/proc_caller.rb b/test/ruby/box/proc_caller.rb new file mode 100644 index 0000000000..8acf538fc1 --- /dev/null +++ b/test/ruby/box/proc_caller.rb @@ -0,0 +1,5 @@ +module Bar + def self.caller(proc_value) + proc_value.call + end +end diff --git a/test/ruby/box/procs.rb b/test/ruby/box/procs.rb new file mode 100644 index 0000000000..1c39a8231b --- /dev/null +++ b/test/ruby/box/procs.rb @@ -0,0 +1,64 @@ +class String + FOO = "foo" + def yay + "yay" + end +end + +module ProcLookupTestA + module B + VALUE = 222 + end +end + +module ProcInBox + def self.make_proc_from_block(&b) + b + end + + def self.call_proc(proc_arg) + proc_arg.call + end + + def self.make_str_proc(type) + case type + when :proc_new then Proc.new { String.new.yay } + when :proc_f then proc { String.new.yay } + when :lambda_f then lambda { String.new.yay } + when :lambda_l then ->(){ String.new.yay } + when :block then make_proc_from_block { String.new.yay } + else + raise "invalid type :#{type}" + end + end + + def self.make_const_proc(type) + case type + when :proc_new then Proc.new { ProcLookupTestA::B::VALUE } + when :proc_f then proc { ProcLookupTestA::B::VALUE } + when :lambda_f then lambda { ProcLookupTestA::B::VALUE } + when :lambda_l then ->(){ ProcLookupTestA::B::VALUE } + when :block then make_proc_from_block { ProcLookupTestA::B::VALUE } + else + raise "invalid type :#{type}" + end + end + + def self.make_str_const_proc(type) + case type + when :proc_new then Proc.new { String::FOO } + when :proc_f then proc { String::FOO } + when :lambda_f then lambda { String::FOO } + when :lambda_l then ->(){ String::FOO } + when :block then make_proc_from_block { String::FOO } + else + raise "invalid type :#{type}" + end + end + + CONST_PROC_NEW = Proc.new { [String.new.yay, String::FOO, ProcLookupTestA::B::VALUE.to_s].join(',') } + CONST_PROC_F = proc { [String.new.yay, String::FOO, ProcLookupTestA::B::VALUE.to_s].join(',') } + CONST_LAMBDA_F = lambda { [String.new.yay, String::FOO, ProcLookupTestA::B::VALUE.to_s].join(',') } + CONST_LAMBDA_L = ->() { [String.new.yay, String::FOO, ProcLookupTestA::B::VALUE.to_s].join(',') } + CONST_BLOCK = make_proc_from_block { [String.new.yay, String::FOO, ProcLookupTestA::B::VALUE.to_s].join(',') } +end diff --git a/test/ruby/box/raise.rb b/test/ruby/box/raise.rb new file mode 100644 index 0000000000..efb67f85c5 --- /dev/null +++ b/test/ruby/box/raise.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +raise "Yay!" diff --git a/test/ruby/box/returns_proc.rb b/test/ruby/box/returns_proc.rb new file mode 100644 index 0000000000..bb816e5024 --- /dev/null +++ b/test/ruby/box/returns_proc.rb @@ -0,0 +1,12 @@ +module Foo + def self.foo + "fooooo" + end + + def self.callee + lambda do + Foo.foo + end + end +end + diff --git a/test/ruby/box/singleton_methods.rb b/test/ruby/box/singleton_methods.rb new file mode 100644 index 0000000000..05470932d2 --- /dev/null +++ b/test/ruby/box/singleton_methods.rb @@ -0,0 +1,65 @@ +class String + def self.greeting + "Good evening!" + end +end + +class Integer + class << self + def answer + 42 + end + end +end + +class Array + def a + size + end + def self.blank + [] + end + def b + size + end +end + +class Hash + def a + size + end + class << self + def http_200 + {status: 200, body: 'OK'} + end + end + def b + size + end +end + +module SingletonMethods + def self.string_greeing + String.greeting + end + + def self.integer_answer + Integer.answer + end + + def self.array_blank + Array.blank + end + + def self.hash_http_200 + Hash.http_200 + end + + def self.array_instance_methods_return_size(ary) + [ary.a, ary.b] + end + + def self.hash_instance_methods_return_size(hash) + [hash.a, hash.b] + end +end diff --git a/test/ruby/box/string_ext.rb b/test/ruby/box/string_ext.rb new file mode 100644 index 0000000000..d8c5a3d661 --- /dev/null +++ b/test/ruby/box/string_ext.rb @@ -0,0 +1,13 @@ +class String + def yay + "yay" + end +end + +String.new.yay # check this doesn't raise NoMethodError + +module Bar + def self.yay + String.new.yay + end +end diff --git a/test/ruby/box/string_ext_caller.rb b/test/ruby/box/string_ext_caller.rb new file mode 100644 index 0000000000..b8345d98ed --- /dev/null +++ b/test/ruby/box/string_ext_caller.rb @@ -0,0 +1,5 @@ +module Foo + def self.yay + String.new.yay + end +end diff --git a/test/ruby/box/string_ext_calling.rb b/test/ruby/box/string_ext_calling.rb new file mode 100644 index 0000000000..6467b728dd --- /dev/null +++ b/test/ruby/box/string_ext_calling.rb @@ -0,0 +1 @@ +Foo.yay diff --git a/test/ruby/box/string_ext_eval_caller.rb b/test/ruby/box/string_ext_eval_caller.rb new file mode 100644 index 0000000000..0e6b20c19f --- /dev/null +++ b/test/ruby/box/string_ext_eval_caller.rb @@ -0,0 +1,12 @@ +module Baz + def self.yay + eval 'String.new.yay' + end + + def self.yay_with_binding + suffix = ", yay!" + eval 'String.new.yay + suffix', binding + end +end + +Baz.yay # should not raise NeMethodError diff --git a/test/ruby/box/top_level.rb b/test/ruby/box/top_level.rb new file mode 100644 index 0000000000..90df145578 --- /dev/null +++ b/test/ruby/box/top_level.rb @@ -0,0 +1,33 @@ +def yaaay + "yay!" +end + +module Foo + def self.foo + yaaay + end +end + +eval 'def foo; "foo"; end' + +Foo.foo # Should not raise NameError + +foo + +module Bar + def self.bar + foo + end +end + +Bar.bar + +$def_retval_in_namespace = def boooo + "boo" +end + +module Baz + def self.baz + raise "#{$def_retval_in_namespace}" + end +end 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/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 2b181b1bdc..90d7c04f9b 100644 --- a/test/ruby/test_allocation.rb +++ b/test/ruby/test_allocation.rb @@ -2,6 +2,12 @@ require 'test/unit' class TestAllocation < Test::Unit::TestCase + def setup + # The namespace changes on i686 platform triggers a bug to allocate objects unexpectedly. + # For now, skip these tests only on i686 + pend if RUBY_PLATFORM =~ /^i686/ + end + def munge_checks(checks) checks end @@ -60,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 @@ -523,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) @@ -775,6 +832,7 @@ class TestAllocation < Test::Unit::TestCase def test_no_array_allocation_with_splat_and_nonstatic_keywords check_allocations(<<~RUBY) def self.keyword(a: nil, b: nil#{block}); end + def self.Object; Object end check_allocations(0, 1, "keyword(*nil, a: empty_array#{block})") # LVAR check_allocations(0, 1, "keyword(*empty_array, a: empty_array#{block})") # LVAR @@ -782,7 +840,8 @@ class TestAllocation < Test::Unit::TestCase check_allocations(0, 1, "$x = empty_array; keyword(*empty_array, a: $x#{block})") # GVAR check_allocations(0, 1, "@x = empty_array; keyword(*empty_array, a: @x#{block})") # IVAR check_allocations(0, 1, "self.class.const_set(:X, empty_array); keyword(*empty_array, a: X#{block})") # CONST - check_allocations(0, 1, "keyword(*empty_array, a: Object::X#{block})") # COLON2 + check_allocations(0, 1, "keyword(*empty_array, a: Object::X#{block})") # COLON2 - safe + check_allocations(1, 1, "keyword(*empty_array, a: Object()::X#{block})") # COLON2 - unsafe check_allocations(0, 1, "keyword(*empty_array, a: ::X#{block})") # COLON3 check_allocations(0, 1, "T = self; #{'B = block' unless block.empty?}; class Object; @@x = X; T.keyword(*X, a: @@x#{', &B' unless block.empty?}) end") # CVAR check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: 1#{block})") # INTEGER @@ -799,6 +858,13 @@ class TestAllocation < Test::Unit::TestCase check_allocations(0, 1, "keyword(*empty_array, a: ->{}#{block})") # LAMBDA check_allocations(0, 1, "keyword(*empty_array, a: $1#{block})") # NTH_REF check_allocations(0, 1, "keyword(*empty_array, a: $`#{block})") # BACK_REF + + # LIST: Only 1 array (literal [:c]), not 2 (one for [:c] and one for *empty_array) + check_allocations(1, 1, "keyword(*empty_array, a: empty_array, b: [:c]#{block})") + check_allocations(1, 1, "keyword(*empty_array, a: empty_array, b: [:c, $x]#{block})") + # LIST unsafe: 2 (one for [Object()] and one for *empty_array) + check_allocations(2, 1, "keyword(*empty_array, a: empty_array, b: [Object()]#{block})") + check_allocations(2, 1, "keyword(*empty_array, a: empty_array, b: [:c, $x, Object()]#{block})") RUBY end @@ -844,13 +910,15 @@ class TestAllocation < Test::Unit::TestCase check_allocations(<<~RUBY) keyword = keyword = proc{ |a: nil, b: nil #{block}| } + def self.Object; Object end check_allocations(0, 1, "keyword.(*empty_array, a: empty_array#{block})") # LVAR check_allocations(0, 1, "->{keyword.(*empty_array, a: empty_array#{block})}.call") # DVAR check_allocations(0, 1, "$x = empty_array; keyword.(*empty_array, a: $x#{block})") # GVAR check_allocations(0, 1, "@x = empty_array; keyword.(*empty_array, a: @x#{block})") # IVAR check_allocations(0, 1, "self.class.const_set(:X, empty_array); keyword.(*empty_array, a: X#{block})") # CONST - check_allocations(0, 1, "keyword.(*empty_array, a: Object::X#{block})") # COLON2 + check_allocations(0, 1, "keyword.(*empty_array, a: Object::X#{block})") # COLON2 - safe + check_allocations(1, 1, "keyword.(*empty_array, a: Object()::X#{block})") # COLON2 - unsafe check_allocations(0, 1, "keyword.(*empty_array, a: ::X#{block})") # COLON3 check_allocations(0, 1, "T = keyword; #{'B = block' unless block.empty?}; class Object; @@x = X; T.(*X, a: @@x#{', &B' unless block.empty?}) end") # CVAR check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1#{block})") # INTEGER @@ -867,6 +935,13 @@ class TestAllocation < Test::Unit::TestCase check_allocations(0, 1, "keyword.(*empty_array, a: ->{}#{block})") # LAMBDA check_allocations(0, 1, "keyword.(*empty_array, a: $1#{block})") # NTH_REF check_allocations(0, 1, "keyword.(*empty_array, a: $`#{block})") # BACK_REF + + # LIST safe: Only 1 array (literal [:c]), not 2 (one for [:c] and one for *empty_array) + check_allocations(1, 1, "keyword.(*empty_array, a: empty_array, b: [:c]#{block})") + check_allocations(1, 1, "keyword.(*empty_array, a: empty_array, b: [:c, $x]#{block})") + # LIST unsafe: 2 (one for [:c] and one for *empty_array) + check_allocations(2, 1, "keyword.(*empty_array, a: empty_array, b: [Object()]#{block})") + check_allocations(2, 1, "keyword.(*empty_array, a: empty_array, b: [:c, $x, Object()]#{block})") RUBY end diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 19f79d236d..04e15b6d87 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -1309,32 +1309,7 @@ class TestArray < Test::Unit::TestCase assert_equal(ary.join(':'), ary2.join(':')) assert_not_nil(x =~ /def/) -=begin - skipping "Not tested: - D,d & double-precision float, native format\\ - E & double-precision float, little-endian byte order\\ - e & single-precision float, little-endian byte order\\ - F,f & single-precision float, native format\\ - G & double-precision float, network (big-endian) byte order\\ - g & single-precision float, network (big-endian) byte order\\ - I & unsigned integer\\ - i & integer\\ - L & unsigned long\\ - l & long\\ - - N & long, network (big-endian) byte order\\ - n & short, network (big-endian) byte-order\\ - P & pointer to a structure (fixed-length string)\\ - p & pointer to a null-terminated string\\ - S & unsigned short\\ - s & short\\ - V & long, little-endian byte order\\ - v & short, little-endian byte order\\ - X & back up a byte\\ - x & null byte\\ - Z & ASCII string (null padded, count is width)\\ -" -=end + # more comprehensive tests are in test_pack.rb end def test_pack_with_buffer @@ -1361,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)) @@ -3609,6 +3606,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 37b23e8db5..22ccbfb604 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 @@ -337,6 +337,19 @@ class TestAst < Test::Unit::TestCase assert_parse("END {defined? yield}") end + def test_invalid_yield_no_memory_leak + # [Bug #21383] + assert_no_memory_leak([], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true) + code = proc do + eval("class C; yield; end") + rescue SyntaxError + end + 1_000.times(&code) + begin; + 100_000.times(&code) + end; + end + def test_node_id_for_location omit if ParserSupport.prism_enabled? @@ -352,6 +365,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]' @@ -788,7 +845,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 @@ -865,7 +922,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 @@ -1173,7 +1230,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 @@ -1384,6 +1441,32 @@ dummy assert_locations(node.children[-1].locations, [[1, 0, 1, 16], [1, 0, 1, 5], [1, 8, 1, 9], [1, 13, 1, 16]]) end + def test_colon2_locations + node = ast_parse("A::B") + assert_locations(node.children[-1].locations, [[1, 0, 1, 4], [1, 1, 1, 3], [1, 3, 1, 4]]) + + node = ast_parse("A::B::C") + assert_locations(node.children[-1].locations, [[1, 0, 1, 7], [1, 4, 1, 6], [1, 6, 1, 7]]) + assert_locations(node.children[-1].children[0].locations, [[1, 0, 1, 4], [1, 1, 1, 3], [1, 3, 1, 4]]) + end + + def test_colon3_locations + node = ast_parse("::A") + assert_locations(node.children[-1].locations, [[1, 0, 1, 3], [1, 0, 1, 2], [1, 2, 1, 3]]) + + node = ast_parse("::A::B") + assert_locations(node.children[-1].locations, [[1, 0, 1, 6], [1, 3, 1, 5], [1, 5, 1, 6]]) + assert_locations(node.children[-1].children[0].locations, [[1, 0, 1, 3], [1, 0, 1, 2], [1, 2, 1, 3]]) + end + + def test_defined_locations + node = ast_parse("defined? x") + assert_locations(node.children[-1].locations, [[1, 0, 1, 10], [1, 0, 1, 8]]) + + node = ast_parse("defined?(x)") + assert_locations(node.children[-1].locations, [[1, 0, 1, 11], [1, 0, 1, 8]]) + end + def test_dot2_locations node = ast_parse("1..2") assert_locations(node.children[-1].locations, [[1, 0, 1, 4], [1, 1, 1, 3]]) @@ -1452,6 +1535,11 @@ dummy assert_locations(node.children[-1].locations, [[1, 0, 1, 20], [1, 0, 1, 2], [1, 10, 1, 12], [1, 17, 1, 20]]) end + def test_module_locations + node = ast_parse('module A end') + assert_locations(node.children[-1].locations, [[1, 0, 1, 12], [1, 0, 1, 6], [1, 9, 1, 12]]) + end + def test_if_locations node = ast_parse("if cond then 1 else 2 end") assert_locations(node.children[-1].locations, [[1, 0, 1, 25], [1, 0, 1, 2], [1, 8, 1, 12], [1, 22, 1, 25]]) @@ -1470,6 +1558,20 @@ dummy assert_locations(node.children[-1].children[1].children[0].locations, [[1, 11, 1, 17], [1, 13, 1, 15], nil, nil]) end + def test_in_locations + node = ast_parse("case 1; in 2 then 3; end") + assert_locations(node.children[-1].children[1].locations, [[1, 8, 1, 20], [1, 8, 1, 10], [1, 13, 1, 17], nil]) + + node = ast_parse("1 => a") + assert_locations(node.children[-1].children[1].locations, [[1, 5, 1, 6], nil, nil, [1, 2, 1, 4]]) + + node = ast_parse("1 in a") + assert_locations(node.children[-1].children[1].locations, [[1, 5, 1, 6], [1, 2, 1, 4], nil, nil]) + + node = ast_parse("case 1; in 2; 3; end") + assert_locations(node.children[-1].children[1].locations, [[1, 8, 1, 16], [1, 8, 1, 10], [1, 12, 1, 13], nil]) + end + def test_next_locations node = ast_parse("loop { next 1 }") assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 7, 1, 13], [1, 7, 1, 11]]) @@ -1534,6 +1636,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]]) diff --git a/test/ruby/test_autoload.rb b/test/ruby/test_autoload.rb index ca3e3d5f7f..82bf2d9d2c 100644 --- a/test/ruby/test_autoload.rb +++ b/test/ruby/test_autoload.rb @@ -224,11 +224,18 @@ p Foo::Bar Kernel.module_eval do alias old_require require end + Ruby::Box.module_eval do + alias old_require require + end called_with = [] Kernel.send :define_method, :require do |path| called_with << path old_require path end + Ruby::Box.send :define_method, :require do |path| + called_with << path + old_require path + end yield called_with ensure Kernel.module_eval do @@ -236,6 +243,11 @@ p Foo::Bar alias require old_require undef old_require end + Ruby::Box.module_eval do + undef require + alias require old_require + undef old_require + end end def test_require_implemented_in_ruby_is_called @@ -249,7 +261,8 @@ p Foo::Bar ensure remove_autoload_constant end - assert_equal [file.path], called_with + # .dup to prevent breaking called_with by autoloading pp, etc + assert_equal [file.path], called_with.dup } end end @@ -267,7 +280,8 @@ p Foo::Bar ensure remove_autoload_constant end - assert_equal [a.path, b.path], called_with + # .dup to prevent breaking called_with by autoloading pp, etc + assert_equal [a.path, b.path], called_with.dup end end end @@ -560,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. @@ -599,4 +613,14 @@ p Foo::Bar RUBY 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 fca7b62030..dad7dfcb55 100644 --- a/test/ruby/test_backtrace.rb +++ b/test/ruby/test_backtrace.rb @@ -454,4 +454,16 @@ class TestBacktrace < Test::Unit::TestCase foo::Bar.baz end; end + + def test_backtrace_internal_frame + backtrace = tap { break caller_locations(0) } + assert_equal(__FILE__, backtrace[1].path) # not "<internal:kernel>" + assert_equal("Kernel#tap", backtrace[1].label) + end + + def test_backtrace_on_argument_error + lineno = __LINE__; [1, 2].inject(:tap) + rescue ArgumentError + assert_equal("#{ __FILE__ }:#{ lineno }:in 'Kernel#tap'", $!.backtrace[0].to_s) + end end diff --git a/test/ruby/test_bignum.rb b/test/ruby/test_bignum.rb index beef33e2a6..c366f794b2 100644 --- a/test/ruby/test_bignum.rb +++ b/test/ruby/test_bignum.rb @@ -605,6 +605,49 @@ class TestBignum < Test::Unit::TestCase assert_equal(1, (-2**(BIGNUM_MIN_BITS*4))[BIGNUM_MIN_BITS*4]) end + def test_aref2 + x = (0x123456789abcdef << (BIGNUM_MIN_BITS + 32)) | 0x12345678 + assert_equal(x, x[0, x.bit_length]) + assert_equal(x >> 10, x[10, x.bit_length]) + assert_equal(0x45678, x[0, 20]) + assert_equal(0x6780, x[-4, 16]) + assert_equal(0x123456, x[x.bit_length - 21, 40]) + assert_equal(0x6789ab, x[x.bit_length - 41, 24]) + assert_equal(0, x[-20, 10]) + assert_equal(0, x[x.bit_length + 10, 10]) + + assert_equal(0, x[5, 0]) + assert_equal(0, (-x)[5, 0]) + + assert_equal(x >> 5, x[5, -1]) + assert_equal(x << 5, x[-5, -1]) + assert_equal((-x) >> 5, (-x)[5, -1]) + assert_equal((-x) << 5, (-x)[-5, -1]) + + assert_equal(x << 5, x[-5, FIXNUM_MAX]) + assert_equal(x >> 5, x[5, FIXNUM_MAX]) + assert_equal(0, x[FIXNUM_MIN, 100]) + assert_equal(0, (-x)[FIXNUM_MIN, 100]) + + y = (x << 160) | 0x1234_0000_0000_0000_1234_0000_0000_0000 + assert_equal(0xffffedcc00, (-y)[40, 40]) + assert_equal(0xfffffffedc, (-y)[52, 40]) + assert_equal(0xffffedcbff, (-y)[104, 40]) + assert_equal(0xfffff6e5d4, (-y)[y.bit_length - 20, 40]) + assert_equal(0, (-y)[-20, 10]) + assert_equal(0xfff, (-y)[y.bit_length + 10, 12]) + + z = (1 << (BIGNUM_MIN_BITS * 2)) - 1 + assert_equal(0x400, (-z)[-10, 20]) + assert_equal(1, (-z)[0, 20]) + assert_equal(0, (-z)[10, 20]) + assert_equal(1, (-z)[0, z.bit_length]) + assert_equal(0, (-z)[z.bit_length - 10, 10]) + assert_equal(0x400, (-z)[z.bit_length - 10, 11]) + assert_equal(0xfff, (-z)[z.bit_length, 12]) + assert_equal(0xfff00, (-z)[z.bit_length - 8, 20]) + end + def test_hash assert_nothing_raised { T31P.hash } end @@ -778,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..bb98a2efbe --- /dev/null +++ b/test/ruby/test_box.rb @@ -0,0 +1,874 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'rbconfig' + +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_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_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_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_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 +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 456362ef21..8f12e06685 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) } @@ -283,12 +323,8 @@ class TestClass < Test::Unit::TestCase assert_raise(TypeError, bug6863) { Class.new(Class.allocate) } allocator = Class.instance_method(:allocate) - assert_raise_with_message(TypeError, /prohibited/) { - allocator.bind(Rational).call - } - assert_raise_with_message(TypeError, /prohibited/) { - allocator.bind_call(Rational) - } + assert_nothing_raised { allocator.bind(Rational).call } + assert_nothing_raised { allocator.bind_call(Rational) } end def test_nonascii_name @@ -565,7 +601,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 @@ -700,9 +736,11 @@ class TestClass < Test::Unit::TestCase def test_namescope_error_message m = Module.new o = m.module_eval "class A\u{3042}; self; end.new" - assert_raise_with_message(TypeError, /A\u{3042}/) { - o::Foo - } + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(TypeError, /A\u{3042}/) { + o::Foo + } + end end def test_redefinition_mismatch @@ -841,4 +879,70 @@ CODE klass.define_method(:bar) {} assert_equal klass, klass.remove_method(:bar), '[Bug #19164]' end + + def test_method_table_assignment_just_after_class_init + assert_normal_exit "#{<<~"begin;"}\n#{<<~'end;'}", 'm_tbl assignment should be done only when Class object is not promoted' + begin; + GC.stress = true + class C; 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;'}" + 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 819d0d35aa..76b961b37e 100644 --- a/test/ruby/test_compile_prism.rb +++ b/test/ruby/test_compile_prism.rb @@ -1046,13 +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("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 ############################################################################ @@ -2180,6 +2186,56 @@ end RUBY end + def test_ForwardingArgumentsNode_instruction_sequence_consistency + # Test that both parsers generate identical instruction sequences for forwarding arguments + # This prevents regressions like the one fixed in prism_compile.c for PM_FORWARDING_ARGUMENTS_NODE + + # Test case from the bug report: def bar(buz, ...) = foo(buz, ...) + source = <<~RUBY + def foo(*, &block) = block + def bar(buz, ...) = foo(buz, ...) + RUBY + + compare_instruction_sequences(source) + + # Test simple forwarding + source = <<~RUBY + def target(...) = nil + def forwarder(...) = target(...) + RUBY + + compare_instruction_sequences(source) + + # Test mixed forwarding with regular arguments + source = <<~RUBY + def target(a, b, c) = [a, b, c] + def forwarder(x, ...) = target(x, ...) + RUBY + + compare_instruction_sequences(source) + + # Test forwarding with splat + source = <<~RUBY + def target(a, b, c) = [a, b, c] + def forwarder(x, ...); target(*x, ...); end + RUBY + + compare_instruction_sequences(source) + end + + private + + def compare_instruction_sequences(source) + # Get instruction sequences from both parsers + parsey_iseq = RubyVM::InstructionSequence.compile_parsey(source) + prism_iseq = RubyVM::InstructionSequence.compile_prism(source) + + # Compare instruction sequences + assert_equal parsey_iseq.disasm, prism_iseq.disasm + end + + public + def test_ForwardingSuperNode assert_prism_eval("class Forwarding; def to_s; super; end; end") assert_prism_eval("class Forwarding; def eval(code); super { code }; end; end") diff --git a/test/ruby/test_data.rb b/test/ruby/test_data.rb index bb38f8ec91..fbc3205d63 100644 --- a/test/ruby/test_data.rb +++ b/test/ruby/test_data.rb @@ -259,9 +259,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 @@ -280,4 +281,10 @@ class TestData < Test::Unit::TestCase assert_not_same(test, loaded) assert_predicate(loaded, :frozen?) end + + def test_frozen_subclass + test = Class.new(Data.define(:a)).freeze.new(a: 0) + assert_kind_of(Data, test) + assert_equal([:a], test.members) + end end diff --git a/test/ruby/test_encoding.rb b/test/ruby/test_encoding.rb index 388b94df39..0cd5bf49dc 100644 --- a/test/ruby/test_encoding.rb +++ b/test/ruby/test_encoding.rb @@ -33,7 +33,7 @@ class TestEncoding < Test::Unit::TestCase encodings.each do |e| assert_raise(TypeError) { e.dup } assert_raise(TypeError) { e.clone } - assert_equal(e.object_id, Marshal.load(Marshal.dump(e)).object_id) + assert_same(e, Marshal.load(Marshal.dump(e))) end end @@ -130,10 +130,50 @@ class TestEncoding < Test::Unit::TestCase def test_ractor_load_encoding assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") begin; - Ractor.new{}.take + Ractor.new{}.join $-w = nil Encoding.default_external = Encoding::ISO8859_2 assert "[Bug #19562]" end; end + + def test_ractor_lazy_load_encoding_concurrently + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + rs = [] + autoload_encodings = Encoding.list.select { |e| e.inspect.include?("(autoload)") }.freeze + 7.times do + rs << Ractor.new(autoload_encodings) do |encodings| + str = "abc".dup + encodings.each do |enc| + str.force_encoding(enc) + end + end + end + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + assert_empty rs + end; + end + + def test_ractor_set_default_external_string + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + $-w = nil + rs = [] + 7.times do |i| + rs << Ractor.new(i) do |i| + Encoding.default_external = "us-ascii" + end + end + + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + assert_empty rs + end; + end end 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 c9ec920ea9..d17e300bce 100644 --- a/test/ruby/test_env.rb +++ b/test/ruby/test_env.rb @@ -601,13 +601,13 @@ class TestEnv < Test::Unit::TestCase rescue Exception => e #{exception_var} = e end - Ractor.yield #{exception_var}.class + port.send #{exception_var}.class end; end def str_for_assert_raise_on_yielded_exception_class(expected_error_class, ractor_var) <<-"end;" - error_class = #{ractor_var}.take + error_class = #{ractor_var}.receive assert_raise(#{expected_error_class}) do if error_class < Exception raise error_class @@ -649,100 +649,101 @@ class TestEnv < Test::Unit::TestCase def test_bracket_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do - Ractor.yield ENV['test'] - Ractor.yield ENV['TEST'] + Ractor.new port = Ractor::Port.new do |port| + port << ENV['test'] + port << ENV['TEST'] ENV['test'] = 'foo' - Ractor.yield ENV['test'] - Ractor.yield ENV['TEST'] + port << ENV['test'] + port << ENV['TEST'] ENV['TEST'] = 'bar' - Ractor.yield ENV['TEST'] - Ractor.yield ENV['test'] + port << ENV['TEST'] + port << ENV['test'] #{str_for_yielding_exception_class("ENV[1]")} #{str_for_yielding_exception_class("ENV[1] = 'foo'")} #{str_for_yielding_exception_class("ENV['test'] = 0")} end - assert_nil(r.take) - assert_nil(r.take) - assert_equal('foo', r.take) + assert_nil(port.receive) + assert_nil(port.receive) + assert_equal('foo', port.receive) if #{ignore_case_str} - assert_equal('foo', r.take) + assert_equal('foo', port.receive) else - assert_nil(r.take) + assert_nil(port.receive) end - assert_equal('bar', r.take) + assert_equal('bar', port.receive) if #{ignore_case_str} - assert_equal('bar', r.take) + assert_equal('bar', port.receive) else - assert_equal('foo', r.take) + assert_equal('foo', port.receive) end 3.times do - #{str_for_assert_raise_on_yielded_exception_class(TypeError, "r")} + #{str_for_assert_raise_on_yielded_exception_class(TypeError, "port")} end end; end def test_dup_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| #{str_for_yielding_exception_class("ENV.dup")} end - #{str_for_assert_raise_on_yielded_exception_class(TypeError, "r")} + #{str_for_assert_raise_on_yielded_exception_class(TypeError, "port")} end; end def test_has_value_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + port = Ractor::Port.new + Ractor.new port do |port| val = 'a' val.succ! while ENV.has_value?(val) || ENV.has_value?(val.upcase) ENV['test'] = val[0...-1] - Ractor.yield(ENV.has_value?(val)) - Ractor.yield(ENV.has_value?(val.upcase)) + port.send(ENV.has_value?(val)) + port.send(ENV.has_value?(val.upcase)) ENV['test'] = val - Ractor.yield(ENV.has_value?(val)) - Ractor.yield(ENV.has_value?(val.upcase)) + port.send(ENV.has_value?(val)) + port.send(ENV.has_value?(val.upcase)) ENV['test'] = val.upcase - Ractor.yield ENV.has_value?(val) - Ractor.yield ENV.has_value?(val.upcase) - end - assert_equal(false, r.take) - assert_equal(false, r.take) - assert_equal(true, r.take) - assert_equal(false, r.take) - assert_equal(false, r.take) - assert_equal(true, r.take) + port.send ENV.has_value?(val) + port.send ENV.has_value?(val.upcase) + end + assert_equal(false, port.receive) + assert_equal(false, port.receive) + assert_equal(true, port.receive) + assert_equal(false, port.receive) + assert_equal(false, port.receive) + assert_equal(true, port.receive) end; end def test_key_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| val = 'a' val.succ! while ENV.has_value?(val) || ENV.has_value?(val.upcase) ENV['test'] = val[0...-1] - Ractor.yield ENV.key(val) - Ractor.yield ENV.key(val.upcase) + port.send ENV.key(val) + port.send ENV.key(val.upcase) ENV['test'] = val - Ractor.yield ENV.key(val) - Ractor.yield ENV.key(val.upcase) + port.send ENV.key(val) + port.send ENV.key(val.upcase) ENV['test'] = val.upcase - Ractor.yield ENV.key(val) - Ractor.yield ENV.key(val.upcase) + port.send ENV.key(val) + port.send ENV.key(val.upcase) end - assert_nil(r.take) - assert_nil(r.take) + assert_nil(port.receive) + assert_nil(port.receive) if #{ignore_case_str} - assert_equal('TEST', r.take.upcase) + assert_equal('TEST', port.receive.upcase) else - assert_equal('test', r.take) + assert_equal('test', port.receive) end - assert_nil(r.take) - assert_nil(r.take) + assert_nil(port.receive) + assert_nil(port.receive) if #{ignore_case_str} - assert_equal('TEST', r.take.upcase) + assert_equal('TEST', port.receive.upcase) else - assert_equal('test', r.take) + assert_equal('test', port.receive) end end; @@ -750,87 +751,87 @@ class TestEnv < Test::Unit::TestCase def test_delete_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| #{str_to_yield_invalid_envvar_errors("v", "ENV.delete(v)")} - Ractor.yield ENV.delete("TEST") + port.send ENV.delete("TEST") #{str_for_yielding_exception_class("ENV.delete('#{PATH_ENV}')")} - Ractor.yield(ENV.delete("TEST"){|name| "NO "+name}) + port.send(ENV.delete("TEST"){|name| "NO "+name}) end - #{str_to_receive_invalid_envvar_errors("r")} - assert_nil(r.take) - exception_class = r.take + #{str_to_receive_invalid_envvar_errors("port")} + assert_nil(port.receive) + exception_class = port.receive assert_equal(NilClass, exception_class) - assert_equal("NO TEST", r.take) + assert_equal("NO TEST", port.receive) end; end def test_getenv_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| #{str_to_yield_invalid_envvar_errors("v", "ENV[v]")} ENV["#{PATH_ENV}"] = "" - Ractor.yield ENV["#{PATH_ENV}"] - Ractor.yield ENV[""] + port.send ENV["#{PATH_ENV}"] + port.send ENV[""] end - #{str_to_receive_invalid_envvar_errors("r")} - assert_equal("", r.take) - assert_nil(r.take) + #{str_to_receive_invalid_envvar_errors("port")} + assert_equal("", port.receive) + assert_nil(port.receive) end; end def test_fetch_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV["test"] = "foo" - Ractor.yield ENV.fetch("test") + port.send ENV.fetch("test") ENV.delete("test") #{str_for_yielding_exception_class("ENV.fetch('test')", exception_var: "ex")} - Ractor.yield ex.receiver.object_id - Ractor.yield ex.key - Ractor.yield ENV.fetch("test", "foo") - Ractor.yield(ENV.fetch("test"){"bar"}) + port.send ex.receiver.object_id + port.send ex.key + port.send ENV.fetch("test", "foo") + port.send(ENV.fetch("test"){"bar"}) #{str_to_yield_invalid_envvar_errors("v", "ENV.fetch(v)")} #{str_for_yielding_exception_class("ENV.fetch('#{PATH_ENV}', 'foo')")} ENV['#{PATH_ENV}'] = "" - Ractor.yield ENV.fetch('#{PATH_ENV}') - end - assert_equal("foo", r.take) - #{str_for_assert_raise_on_yielded_exception_class(KeyError, "r")} - assert_equal(ENV.object_id, r.take) - assert_equal("test", r.take) - assert_equal("foo", r.take) - assert_equal("bar", r.take) - #{str_to_receive_invalid_envvar_errors("r")} - exception_class = r.take + port.send ENV.fetch('#{PATH_ENV}') + end + assert_equal("foo", port.receive) + #{str_for_assert_raise_on_yielded_exception_class(KeyError, "port")} + assert_equal(ENV.object_id, port.receive) + assert_equal("test", port.receive) + assert_equal("foo", port.receive) + assert_equal("bar", port.receive) + #{str_to_receive_invalid_envvar_errors("port")} + exception_class = port.receive assert_equal(NilClass, exception_class) - assert_equal("", r.take) + assert_equal("", port.receive) end; end def test_aset_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| #{str_for_yielding_exception_class("ENV['test'] = nil")} ENV["test"] = nil - Ractor.yield ENV["test"] + port.send ENV["test"] #{str_to_yield_invalid_envvar_errors("v", "ENV[v] = 'test'")} #{str_to_yield_invalid_envvar_errors("v", "ENV['test'] = v")} end - exception_class = r.take + exception_class = port.receive assert_equal(NilClass, exception_class) - assert_nil(r.take) - #{str_to_receive_invalid_envvar_errors("r")} - #{str_to_receive_invalid_envvar_errors("r")} + assert_nil(port.receive) + #{str_to_receive_invalid_envvar_errors("port")} + #{str_to_receive_invalid_envvar_errors("port")} end; end def test_keys_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| a = ENV.keys - Ractor.yield a + port.send a end - a = r.take + a = port.receive assert_kind_of(Array, a) a.each {|k| assert_kind_of(String, k) } end; @@ -839,11 +840,11 @@ class TestEnv < Test::Unit::TestCase def test_each_key_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do - ENV.each_key {|k| Ractor.yield(k)} - Ractor.yield "finished" + Ractor.new port = Ractor::Port.new do |port| + ENV.each_key {|k| port.send(k)} + port.send "finished" end - while((x=r.take) != "finished") + while((x=port.receive) != "finished") assert_kind_of(String, x) end end; @@ -851,11 +852,11 @@ class TestEnv < Test::Unit::TestCase def test_values_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| a = ENV.values - Ractor.yield a + port.send a end - a = r.take + a = port.receive assert_kind_of(Array, a) a.each {|k| assert_kind_of(String, k) } end; @@ -863,11 +864,11 @@ class TestEnv < Test::Unit::TestCase def test_each_value_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do - ENV.each_value {|k| Ractor.yield(k)} - Ractor.yield "finished" + Ractor.new port = Ractor::Port.new do |port| + ENV.each_value {|k| port.send(k)} + port.send "finished" end - while((x=r.take) != "finished") + while((x=port.receive) != "finished") assert_kind_of(String, x) end end; @@ -875,11 +876,11 @@ class TestEnv < Test::Unit::TestCase def test_each_pair_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do - ENV.each_pair {|k, v| Ractor.yield([k,v])} - Ractor.yield "finished" + Ractor.new port = Ractor::Port.new do |port| + ENV.each_pair {|k, v| port.send([k,v])} + port.send "finished" end - while((k,v=r.take) != "finished") + while((k,v=port.receive) != "finished") assert_kind_of(String, k) assert_kind_of(String, v) end @@ -888,116 +889,116 @@ class TestEnv < Test::Unit::TestCase def test_reject_bang_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| h1 = {} ENV.each_pair {|k, v| h1[k] = v } ENV["test"] = "foo" ENV.reject! {|k, v| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" } h2 = {} ENV.each_pair {|k, v| h2[k] = v } - Ractor.yield [h1, h2] - Ractor.yield(ENV.reject! {|k, v| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" }) + port.send [h1, h2] + port.send(ENV.reject! {|k, v| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" }) end - h1, h2 = r.take + h1, h2 = port.receive assert_equal(h1, h2) - assert_nil(r.take) + assert_nil(port.receive) end; end def test_delete_if_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| h1 = {} ENV.each_pair {|k, v| h1[k] = v } ENV["test"] = "foo" ENV.delete_if {|k, v| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" } h2 = {} ENV.each_pair {|k, v| h2[k] = v } - Ractor.yield [h1, h2] - Ractor.yield (ENV.delete_if {|k, v| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" }).object_id + port.send [h1, h2] + port.send (ENV.delete_if {|k, v| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" }) end - h1, h2 = r.take + h1, h2 = port.receive assert_equal(h1, h2) - assert_equal(ENV.object_id, r.take) + assert_same(ENV, port.receive) end; end def test_select_bang_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| h1 = {} ENV.each_pair {|k, v| h1[k] = v } ENV["test"] = "foo" ENV.select! {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" } h2 = {} ENV.each_pair {|k, v| h2[k] = v } - Ractor.yield [h1, h2] - Ractor.yield(ENV.select! {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" }) + port.send [h1, h2] + port.send(ENV.select! {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" }) end - h1, h2 = r.take + h1, h2 = port.receive assert_equal(h1, h2) - assert_nil(r.take) + assert_nil(port.receive) end; end def test_filter_bang_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| h1 = {} ENV.each_pair {|k, v| h1[k] = v } ENV["test"] = "foo" ENV.filter! {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" } h2 = {} ENV.each_pair {|k, v| h2[k] = v } - Ractor.yield [h1, h2] - Ractor.yield(ENV.filter! {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" }) + port.send [h1, h2] + port.send(ENV.filter! {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" }) end - h1, h2 = r.take + h1, h2 = port.receive assert_equal(h1, h2) - assert_nil(r.take) + assert_nil(port.receive) end; end def test_keep_if_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| h1 = {} ENV.each_pair {|k, v| h1[k] = v } ENV["test"] = "foo" ENV.keep_if {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" } h2 = {} ENV.each_pair {|k, v| h2[k] = v } - Ractor.yield [h1, h2] - Ractor.yield (ENV.keep_if {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" }).object_id + port.send [h1, h2] + port.send (ENV.keep_if {|k, v| #{ignore_case_str} ? k.upcase != "TEST" : k != "test" }) end - h1, h2 = r.take + h1, h2 = port.receive assert_equal(h1, h2) - assert_equal(ENV.object_id, r.take) + assert_equal(ENV, port.receive) end; end def test_values_at_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV["test"] = "foo" - Ractor.yield ENV.values_at("test", "test") + port.send ENV.values_at("test", "test") end - assert_equal(["foo", "foo"], r.take) + assert_equal(["foo", "foo"], port.receive) end; end def test_select_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV["test"] = "foo" h = ENV.select {|k| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" } - Ractor.yield h.size + port.send h.size k = h.keys.first v = h.values.first - Ractor.yield [k, v] + port.send [k, v] end - assert_equal(1, r.take) - k, v = r.take + assert_equal(1, port.receive) + k, v = port.receive if #{ignore_case_str} assert_equal("TEST", k.upcase) assert_equal("FOO", v.upcase) @@ -1010,16 +1011,16 @@ class TestEnv < Test::Unit::TestCase def test_filter_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV["test"] = "foo" h = ENV.filter {|k| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" } - Ractor.yield(h.size) + port.send(h.size) k = h.keys.first v = h.values.first - Ractor.yield [k, v] + port.send [k, v] end - assert_equal(1, r.take) - k, v = r.take + assert_equal(1, port.receive) + k, v = port.receive if #{ignore_case_str} assert_equal("TEST", k.upcase) assert_equal("FOO", v.upcase) @@ -1032,49 +1033,49 @@ class TestEnv < Test::Unit::TestCase def test_slice_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" ENV["bar"] = "rab" - Ractor.yield(ENV.slice()) - Ractor.yield(ENV.slice("")) - Ractor.yield(ENV.slice("unknown")) - Ractor.yield(ENV.slice("foo", "baz")) - end - assert_equal({}, r.take) - assert_equal({}, r.take) - assert_equal({}, r.take) - assert_equal({"foo"=>"bar", "baz"=>"qux"}, r.take) + port.send(ENV.slice()) + port.send(ENV.slice("")) + port.send(ENV.slice("unknown")) + port.send(ENV.slice("foo", "baz")) + end + assert_equal({}, port.receive) + assert_equal({}, port.receive) + assert_equal({}, port.receive) + assert_equal({"foo"=>"bar", "baz"=>"qux"}, port.receive) end; end def test_except_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" ENV["bar"] = "rab" - Ractor.yield ENV.except() - Ractor.yield ENV.except("") - Ractor.yield ENV.except("unknown") - Ractor.yield ENV.except("foo", "baz") - end - assert_equal({"bar"=>"rab", "baz"=>"qux", "foo"=>"bar"}, r.take) - assert_equal({"bar"=>"rab", "baz"=>"qux", "foo"=>"bar"}, r.take) - assert_equal({"bar"=>"rab", "baz"=>"qux", "foo"=>"bar"}, r.take) - assert_equal({"bar"=>"rab"}, r.take) + port.send ENV.except() + port.send ENV.except("") + port.send ENV.except("unknown") + port.send ENV.except("foo", "baz") + end + assert_equal({"bar"=>"rab", "baz"=>"qux", "foo"=>"bar"}, port.receive) + assert_equal({"bar"=>"rab", "baz"=>"qux", "foo"=>"bar"}, port.receive) + assert_equal({"bar"=>"rab", "baz"=>"qux", "foo"=>"bar"}, port.receive) + assert_equal({"bar"=>"rab"}, port.receive) end; end def test_clear_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear - Ractor.yield ENV.size + port.send ENV.size end - assert_equal(0, r.take) + assert_equal(0, port.receive) end; end @@ -1083,20 +1084,20 @@ class TestEnv < Test::Unit::TestCase r = Ractor.new do ENV.to_s end - assert_equal("ENV", r.take) + assert_equal("ENV", r.value) end; end def test_inspect_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" s = ENV.inspect - Ractor.yield s + port.send s end - s = r.take + s = port.receive expected = ['"foo" => "bar"', '"baz" => "qux"'] unless s.start_with?(/\{"foo"/i) expected.reverse! @@ -1112,14 +1113,14 @@ class TestEnv < Test::Unit::TestCase def test_to_a_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" a = ENV.to_a - Ractor.yield a + port.send a end - a = r.take + a = port.receive assert_equal(2, a.size) expected = [%w(baz qux), %w(foo bar)] if #{ignore_case_str} @@ -1136,59 +1137,59 @@ class TestEnv < Test::Unit::TestCase r = Ractor.new do ENV.rehash end - assert_nil(r.take) + assert_nil(r.value) end; end def test_size_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| s = ENV.size ENV["test"] = "foo" - Ractor.yield [s, ENV.size] + port.send [s, ENV.size] end - s, s2 = r.take + s, s2 = port.receive assert_equal(s + 1, s2) end; end def test_empty_p_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear - Ractor.yield ENV.empty? + port.send ENV.empty? ENV["test"] = "foo" - Ractor.yield ENV.empty? + port.send ENV.empty? end - assert r.take - assert !r.take + assert port.receive + assert !port.receive end; end def test_has_key_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do - Ractor.yield ENV.has_key?("test") + Ractor.new port = Ractor::Port.new do |port| + port.send ENV.has_key?("test") ENV["test"] = "foo" - Ractor.yield ENV.has_key?("test") + port.send ENV.has_key?("test") #{str_to_yield_invalid_envvar_errors("v", "ENV.has_key?(v)")} end - assert !r.take - assert r.take - #{str_to_receive_invalid_envvar_errors("r")} + assert !port.receive + assert port.receive + #{str_to_receive_invalid_envvar_errors("port")} end; end def test_assoc_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do - Ractor.yield ENV.assoc("test") + Ractor.new port = Ractor::Port.new do |port| + port.send ENV.assoc("test") ENV["test"] = "foo" - Ractor.yield ENV.assoc("test") + port.send ENV.assoc("test") #{str_to_yield_invalid_envvar_errors("v", "ENV.assoc(v)")} end - assert_nil(r.take) - k, v = r.take + assert_nil(port.receive) + k, v = port.receive if #{ignore_case_str} assert_equal("TEST", k.upcase) assert_equal("FOO", v.upcase) @@ -1196,7 +1197,7 @@ class TestEnv < Test::Unit::TestCase assert_equal("test", k) assert_equal("foo", v) end - #{str_to_receive_invalid_envvar_errors("r")} + #{str_to_receive_invalid_envvar_errors("port")} encoding = /mswin|mingw/ =~ RUBY_PLATFORM ? Encoding::UTF_8 : Encoding.find("locale") assert_equal(encoding, v.encoding) end; @@ -1204,29 +1205,29 @@ class TestEnv < Test::Unit::TestCase def test_has_value2_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear - Ractor.yield ENV.has_value?("foo") + port.send ENV.has_value?("foo") ENV["test"] = "foo" - Ractor.yield ENV.has_value?("foo") + port.send ENV.has_value?("foo") end - assert !r.take - assert r.take + assert !port.receive + assert port.receive end; end def test_rassoc_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear - Ractor.yield ENV.rassoc("foo") + port.send ENV.rassoc("foo") ENV["foo"] = "bar" ENV["test"] = "foo" ENV["baz"] = "qux" - Ractor.yield ENV.rassoc("foo") + port.send ENV.rassoc("foo") end - assert_nil(r.take) - k, v = r.take + assert_nil(port.receive) + k, v = port.receive if #{ignore_case_str} assert_equal("TEST", k.upcase) assert_equal("FOO", v.upcase) @@ -1239,39 +1240,39 @@ class TestEnv < Test::Unit::TestCase def test_to_hash_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| h = {} ENV.each {|k, v| h[k] = v } - Ractor.yield [h, ENV.to_hash] + port.send [h, ENV.to_hash] end - h, h2 = r.take + h, h2 = port.receive assert_equal(h, h2) end; end def test_to_h_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do - Ractor.yield [ENV.to_hash, ENV.to_h] - Ractor.yield [ENV.map {|k, v| ["$\#{k}", v.size]}.to_h, ENV.to_h {|k, v| ["$\#{k}", v.size]}] + Ractor.new port = Ractor::Port.new do |port| + port.send [ENV.to_hash, ENV.to_h] + port.send [ENV.map {|k, v| ["$\#{k}", v.size]}.to_h, ENV.to_h {|k, v| ["$\#{k}", v.size]}] end - a, b = r.take + a, b = port.receive assert_equal(a,b) - c, d = r.take + c, d = port.receive assert_equal(c,d) end; end def test_reject_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| h1 = {} ENV.each_pair {|k, v| h1[k] = v } ENV["test"] = "foo" h2 = ENV.reject {|k, v| #{ignore_case_str} ? k.upcase == "TEST" : k == "test" } - Ractor.yield [h1, h2] + port.send [h1, h2] end - h1, h2 = r.take + h1, h2 = port.receive assert_equal(h1, h2) end; end @@ -1279,86 +1280,86 @@ class TestEnv < Test::Unit::TestCase def test_shift_in_ractor assert_ractor(<<-"end;") #{STR_DEFINITION_FOR_CHECK} - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" a = ENV.shift b = ENV.shift - Ractor.yield [a,b] - Ractor.yield ENV.shift + port.send [a,b] + port.send ENV.shift end - a,b = r.take + a,b = port.receive check([a, b], [%w(foo bar), %w(baz qux)]) - assert_nil(r.take) + assert_nil(port.receive) end; end def test_invert_in_ractor assert_ractor(<<-"end;") #{STR_DEFINITION_FOR_CHECK} - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" - Ractor.yield(ENV.invert) + port.send(ENV.invert) end - check(r.take.to_a, [%w(bar foo), %w(qux baz)]) + check(port.receive.to_a, [%w(bar foo), %w(qux baz)]) end; end def test_replace_in_ractor assert_ractor(<<-"end;") #{STR_DEFINITION_FOR_CHECK} - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV["foo"] = "xxx" ENV.replace({"foo"=>"bar", "baz"=>"qux"}) - Ractor.yield ENV.to_hash + port.send ENV.to_hash ENV.replace({"Foo"=>"Bar", "Baz"=>"Qux"}) - Ractor.yield ENV.to_hash + port.send ENV.to_hash end - check(r.take.to_a, [%w(foo bar), %w(baz qux)]) - check(r.take.to_a, [%w(Foo Bar), %w(Baz Qux)]) + check(port.receive.to_a, [%w(foo bar), %w(baz qux)]) + check(port.receive.to_a, [%w(Foo Bar), %w(Baz Qux)]) end; end def test_update_in_ractor assert_ractor(<<-"end;") #{STR_DEFINITION_FOR_CHECK} - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" ENV.update({"baz"=>"quux","a"=>"b"}) - Ractor.yield ENV.to_hash + port.send ENV.to_hash ENV.clear ENV["foo"] = "bar" ENV["baz"] = "qux" ENV.update({"baz"=>"quux","a"=>"b"}) {|k, v1, v2| k + "_" + v1 + "_" + v2 } - Ractor.yield ENV.to_hash + port.send ENV.to_hash end - check(r.take.to_a, [%w(foo bar), %w(baz quux), %w(a b)]) - check(r.take.to_a, [%w(foo bar), %w(baz baz_qux_quux), %w(a b)]) + check(port.receive.to_a, [%w(foo bar), %w(baz quux), %w(a b)]) + check(port.receive.to_a, [%w(foo bar), %w(baz baz_qux_quux), %w(a b)]) end; end def test_huge_value_in_ractor assert_ractor(<<-"end;") huge_value = "bar" * 40960 - r = Ractor.new huge_value do |v| + Ractor.new port = Ractor::Port.new, huge_value do |port, v| ENV["foo"] = "bar" #{str_for_yielding_exception_class("ENV['foo'] = v ")} - Ractor.yield ENV["foo"] + port.send ENV["foo"] end if /mswin|ucrt/ =~ RUBY_PLATFORM - #{str_for_assert_raise_on_yielded_exception_class(Errno::EINVAL, "r")} - result = r.take + #{str_for_assert_raise_on_yielded_exception_class(Errno::EINVAL, "port")} + result = port.receive assert_equal("bar", result) else - exception_class = r.take + exception_class = port.receive assert_equal(NilClass, exception_class) - result = r.take + result = port.receive assert_equal(huge_value, result) end end; @@ -1366,42 +1367,43 @@ class TestEnv < Test::Unit::TestCase def test_frozen_env_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| #{str_for_yielding_exception_class("ENV.freeze")} end - #{str_for_assert_raise_on_yielded_exception_class(TypeError, "r")} + #{str_for_assert_raise_on_yielded_exception_class(TypeError, "port")} end; end def test_frozen_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| ENV["#{PATH_ENV}"] = "/" ENV.each do |k, v| - Ractor.yield [k.frozen?] - Ractor.yield [v.frozen?] + port.send [k] + port.send [v] end ENV.each_key do |k| - Ractor.yield [k.frozen?] + port.send [k] end ENV.each_value do |v| - Ractor.yield [v.frozen?] + port.send [v] end ENV.each_key do |k| - Ractor.yield [ENV[k].frozen?, "[\#{k.dump}]"] - Ractor.yield [ENV.fetch(k).frozen?, "fetch(\#{k.dump})"] + port.send [ENV[k], "[\#{k.dump}]"] + port.send [ENV.fetch(k), "fetch(\#{k.dump})"] end - Ractor.yield "finished" + port.send "finished" end - while((params=r.take) != "finished") - assert(*params) + while((params=port.receive) != "finished") + value, *params = params + assert_predicate(value, :frozen?, *params) end end; end def test_shared_substring_in_ractor assert_ractor(<<-"end;") - r = Ractor.new do + Ractor.new port = Ractor::Port.new do |port| bug12475 = '[ruby-dev:49655] [Bug #12475]' n = [*"0".."9"].join("")*3 e0 = ENV[n0 = "E\#{n}"] @@ -1411,9 +1413,9 @@ class TestEnv < Test::Unit::TestCase ENV[n1.chop] = "T\#{n}.".chop ENV[n0], e0 = e0, ENV[n0] ENV[n1], e1 = e1, ENV[n1] - Ractor.yield [n, e0, e1, bug12475] + port.send [n, e0, e1, bug12475] end - n, e0, e1, bug12475 = r.take + n, e0, e1, bug12475 = port.receive assert_equal("T\#{n}", e0, bug12475) assert_nil(e1, bug12475) end; @@ -1429,7 +1431,7 @@ class TestEnv < Test::Unit::TestCase rescue Ractor::IsolationError => e e end - assert_equal Ractor::IsolationError, r_get.take.class + assert_equal Ractor::IsolationError, r_get.value.class r_get = Ractor.new do ENV.instance_eval{ @a } @@ -1437,7 +1439,7 @@ class TestEnv < Test::Unit::TestCase e end - assert_equal Ractor::IsolationError, r_get.take.class + assert_equal Ractor::IsolationError, r_get.value.class r_set = Ractor.new do ENV.instance_eval{ @b = "hello" } @@ -1445,7 +1447,7 @@ class TestEnv < Test::Unit::TestCase e end - assert_equal Ractor::IsolationError, r_set.take.class + assert_equal Ractor::IsolationError, r_set.value.class RUBY end diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index 84581180b6..31e5aa9f6b 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 to Array (TestException::Ex#to_a gives Integer)") do + x(*a) + end + assert_raise_with_message(TypeError, "can't convert TestException::Ex to Hash (TestException::Ex#to_hash gives Integer)") do + x(**a) + end + assert_raise_with_message(TypeError, "can't convert TestException::Ex to 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 1f78307748..b7d2b71c19 100644 --- a/test/ruby/test_fiber.rb +++ b/test/ruby/test_fiber.rb @@ -498,7 +498,7 @@ class TestFiber < Test::Unit::TestCase end def test_machine_stack_gc - assert_normal_exit <<-RUBY, '[Bug #14561]', timeout: 10 + assert_normal_exit <<-RUBY, '[Bug #14561]', timeout: 60 enum = Enumerator.new { |y| y << 1 } thread = Thread.new { enum.peek } thread.join diff --git a/test/ruby/test_file.rb b/test/ruby/test_file.rb index eae9a8e7b0..a3d6221c0f 100644 --- a/test/ruby/test_file.rb +++ b/test/ruby/test_file.rb @@ -372,9 +372,9 @@ class TestFile < Test::Unit::TestCase end def test_stat - tb = Process.clock_gettime(Process::CLOCK_REALTIME) + btime = Process.clock_gettime(Process::CLOCK_REALTIME) Tempfile.create("stat") {|file| - tb = (tb + Process.clock_gettime(Process::CLOCK_REALTIME)) / 2 + btime = (btime + Process.clock_gettime(Process::CLOCK_REALTIME)) / 2 file.close path = file.path @@ -384,33 +384,32 @@ class TestFile < Test::Unit::TestCase sleep 2 - t1 = measure_time do + mtime = measure_time do File.write(path, "bar") end sleep 2 - t2 = measure_time do - File.read(path) + ctime = measure_time do File.chmod(0644, path) end sleep 2 - t3 = measure_time do + atime = measure_time do File.read(path) end delta = 1 stat = File.stat(path) - assert_in_delta tb, stat.birthtime.to_f, delta - assert_in_delta t1, stat.mtime.to_f, delta + assert_in_delta btime, stat.birthtime.to_f, delta + assert_in_delta mtime, stat.mtime.to_f, delta if stat.birthtime != stat.ctime - assert_in_delta t2, stat.ctime.to_f, delta + assert_in_delta ctime, stat.ctime.to_f, delta end if /mswin|mingw/ !~ RUBY_PLATFORM && !Bug::File::Fs.noatime?(path) # Windows delays updating atime - assert_in_delta t3, stat.atime.to_f, delta + assert_in_delta atime, stat.atime.to_f, delta end } rescue NotImplementedError diff --git a/test/ruby/test_file_exhaustive.rb b/test/ruby/test_file_exhaustive.rb index bb64483a79..394dc47603 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) @@ -1477,6 +1497,7 @@ class TestFileExhaustive < Test::Unit::TestCase end def test_test + omit 'timestamp check is unstable on macOS' if RUBY_PLATFORM =~ /darwin/ fn1 = regular_file hardlinkfile sleep(1.1) diff --git a/test/ruby/test_float.rb b/test/ruby/test_float.rb index b865d339a9..d0d180593a 100644 --- a/test/ruby/test_float.rb +++ b/test/ruby/test_float.rb @@ -861,7 +861,9 @@ class TestFloat < Test::Unit::TestCase assert_raise(Encoding::CompatibilityError) {Float("0".encode("utf-32le"))} assert_raise(Encoding::CompatibilityError) {Float("0".encode("iso-2022-jp"))} - assert_raise_with_message(ArgumentError, /\u{1f4a1}/) {Float("\u{1f4a1}")} + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(ArgumentError, /\u{1f4a1}/) {Float("\u{1f4a1}")} + end end def test_invalid_str 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 a1229fc87a..09199c34b1 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -75,12 +75,9 @@ class TestGc < Test::Unit::TestCase GC.start end - def test_gc_config_setting_returns_nil_for_missing_keys - missing_value = GC.config(no_such_key: true)[:no_such_key] - assert_nil(missing_value) - ensure - GC.config(full_mark: true) - GC.start + def test_gc_config_setting_returns_config_hash + hash = GC.config(no_such_key: true) + assert_equal(GC.config, hash) end def test_gc_config_disable_major @@ -234,6 +231,9 @@ class TestGc < Test::Unit::TestCase end assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), 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 @@ -264,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 @@ -289,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] @@ -296,7 +299,7 @@ class TestGc < Test::Unit::TestCase end def test_measure_total_time - assert_separately([], __FILE__, __LINE__, <<~RUBY) + assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60) GC.measure_total_time = false time_before = GC.stat(:time) @@ -379,53 +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) - enum = count.times - enum.each.with_index do |i| - obj = Object.new - ary[i] = obj - wmap[obj] = nil + # 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 - GC.start - # Run full GC again to collect stats about weak references + # Free ary, which should GC all the WeakMaps GC.start - # Sometimes the WeakMap has a few elements, which might be held on by registers. - assert_operator(wmap.size, :<=, 2) - - 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 @@ -452,7 +438,7 @@ class TestGc < Test::Unit::TestCase end def test_singleton_method_added - assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:44436]") + assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:44436]", timeout: 30) class BasicObject undef singleton_method_added def singleton_method_added(mid) @@ -467,13 +453,6 @@ 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" @@ -703,7 +682,14 @@ class TestGc < Test::Unit::TestCase allocate_large_object end - assert_operator(GC.stat(:heap_available_slots), :<, COUNT * 2) + # Running GC here is required to prevent this test from being flaky because + # the heap for the small transient objects may not have been cleared by the + # GC causing heap_available_slots to be slightly over 2 * COUNT. + GC.start + + heap_available_slots = GC.stat(:heap_available_slots) + + assert_operator(heap_available_slots, :<, COUNT * 2, "GC.stat: #{GC.stat}\nGC.stat_heap: #{GC.stat_heap}") RUBY end @@ -740,6 +726,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') @@ -755,7 +742,7 @@ class TestGc < Test::Unit::TestCase ObjectSpace.define_finalizer(Object.new, f) end end; - out, err, status = assert_in_out_err(["-e", src], "", [], [], bug10595, signal: :SEGV) do |*result| + out, err, status = assert_in_out_err(["-e", src], "", [], [], bug10595, signal: :SEGV, timeout: 100) do |*result| break result end unless /mswin|mingw/ =~ RUBY_PLATFORM @@ -822,6 +809,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" @@ -842,6 +831,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" @@ -907,4 +898,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') + 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..7e0c499dd9 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 @@ -324,7 +324,7 @@ 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 @@ -356,11 +356,27 @@ 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 - 15) 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[:SIZE_POOL_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 @@ -377,7 +393,7 @@ class TestGCCompact < Test::Unit::TestCase 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 - 15) refute_empty($ary.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end diff --git a/test/ruby/test_hash.rb b/test/ruby/test_hash.rb index b6c18ea958..32384f5a5c 100644 --- a/test/ruby/test_hash.rb +++ b/test/ruby/test_hash.rb @@ -880,21 +880,20 @@ class TestHash < Test::Unit::TestCase assert_equal(quote1, eval(quote1).inspect) assert_equal(quote2, eval(quote2).inspect) assert_equal(quote3, eval(quote3).inspect) - begin - verbose_bak, $VERBOSE = $VERBOSE, nil - enc = Encoding.default_external - Encoding.default_external = Encoding::ASCII + + EnvUtil.with_default_external(Encoding::ASCII) do utf8_ascii_hash = '{"\\u3042": 1}' assert_equal(eval(utf8_ascii_hash).inspect, utf8_ascii_hash) - Encoding.default_external = Encoding::UTF_8 + end + + EnvUtil.with_default_external(Encoding::UTF_8) do utf8_hash = "{\u3042: 1}" assert_equal(eval(utf8_hash).inspect, utf8_hash) - Encoding.default_external = Encoding::Windows_31J + end + + EnvUtil.with_default_external(Encoding::Windows_31J) do sjis_hash = "{\x87]: 1}".force_encoding('sjis') assert_equal(eval(sjis_hash).inspect, sjis_hash) - ensure - Encoding.default_external = enc - $VERBOSE = verbose_bak end end @@ -1297,6 +1296,17 @@ class TestHash < Test::Unit::TestCase assert_equal(@cls[a: 10, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10], h) end + def test_update_modify_in_block + a = @cls[] + (1..1337).each {|k| a[k] = k} + b = {1=>1338} + assert_raise_with_message(RuntimeError, /rehash during iteration/) do + a.update(b) {|k, o, n| + a.rehash + } + end + end + def test_update_on_identhash key = +'a' i = @cls[].compare_by_identity @@ -1853,6 +1863,14 @@ class TestHash < Test::Unit::TestCase end end assert_equal(@cls[a: 2, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10], x) + + x = (1..1337).to_h {|k| [k, k]} + assert_raise_with_message(RuntimeError, /rehash during iteration/) do + x.transform_values! {|v| + x.rehash if v == 1337 + v * 2 + } + end end def hrec h, n, &b @@ -1989,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 @@ -2119,7 +2137,9 @@ class TestHashOnly < Test::Unit::TestCase def test_iterlevel_in_ivar_bug19589 h = { a: nil } - hash_iter_recursion(h, 200) + # Recursion level should be over 127 to actually test iterlevel being set in an instance variable, + # but it should be under 131 not to overflow the stack under MN threads/ractors. + hash_iter_recursion(h, 130) assert true end @@ -2336,6 +2356,11 @@ class TestHashOnly < Test::Unit::TestCase end end + def test_bug_21357 + h = {x: []}.merge(x: nil) { |_k, v1, _v2| v1 } + assert_equal({x: []}, h) + end + def test_any_hash_fixable 20.times do assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") diff --git a/test/ruby/test_integer.rb b/test/ruby/test_integer.rb index fb7aabba35..f9bf4fa20c 100644 --- a/test/ruby/test_integer.rb +++ b/test/ruby/test_integer.rb @@ -158,7 +158,9 @@ class TestInteger < Test::Unit::TestCase assert_raise(Encoding::CompatibilityError, bug6192) {Integer("0".encode("utf-32le"))} assert_raise(Encoding::CompatibilityError, bug6192) {Integer("0".encode("iso-2022-jp"))} - assert_raise_with_message(ArgumentError, /\u{1f4a1}/) {Integer("\u{1f4a1}")} + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(ArgumentError, /\u{1f4a1}/) {Integer("\u{1f4a1}")} + end obj = Struct.new(:s).new(%w[42 not-an-integer]) def obj.to_str; s.shift; end diff --git a/test/ruby/test_io.rb b/test/ruby/test_io.rb index 32d7519bdf..1adf47ac51 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 @@ -2601,36 +2619,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 +2832,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 +2907,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')} @@ -3826,7 +3810,7 @@ __END__ end tempfiles = [] - (0..fd_setsize+1).map {|i| + (0...fd_setsize).map {|i| tempfiles << Tempfile.create("test_io_select_with_many_files") } @@ -4416,4 +4400,29 @@ __END__ end RUBY end + + def test_fork_close + omit "fork is not supported" unless Process.respond_to?(:fork) + + assert_separately([], <<~'RUBY') + r, w = IO.pipe + + thread = Thread.new do + r.read + end + + Thread.pass until thread.status == "sleep" + + pid = fork do + r.close + end + + w.close + + status = Process.wait2(pid).last + thread.join + + assert_predicate(status, :success?) + RUBY + end end diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 55296c1f23..706ce16c42 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) @@ -121,6 +174,16 @@ class TestIOBuffer < Test::Unit::TestCase end end + def test_string_mapped_buffer_frozen + string = "Hello World".freeze + IO::Buffer.for(string) do |buffer| + assert_raise IO::Buffer::AccessError, "Buffer is not writable!" do + buffer.set_string("abc") + end + assert_equal "H".ord, buffer.get_value(:U8, 0) + end + end + def test_non_string not_string = Object.new @@ -343,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) @@ -355,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 @@ -411,6 +491,7 @@ 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_zero_length_each_byte @@ -421,7 +502,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 @@ -620,8 +715,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") @@ -683,4 +778,156 @@ class TestIOBuffer < Test::Unit::TestCase buf.set_string('a', 0, 0) assert_predicate buf, :empty? end + + # https://bugs.ruby-lang.org/issues/21210 + def test_bug_21210 + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + str = +"hello" + buf = IO::Buffer.for(str) + assert_predicate buf, :valid? + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + 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 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 86c1f51dde..fa716787fe 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 - 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 + eval("#{name} = nil; Ractor.shareable_proc{#{name} = nil}") 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 @@ -297,6 +292,56 @@ class TestISeq < Test::Unit::TestCase assert_raise(TypeError, bug11159) {compile(1)} end + def test_invalid_source_no_memory_leak + # [Bug #21394] + assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true) + code = proc do |t| + RubyVM::InstructionSequence.new(nil) + rescue TypeError + else + raise "TypeError was not raised during RubyVM::InstructionSequence.new" + end + + 10.times(&code) + begin; + 1_000_000.times(&code) + end; + + # [Bug #21394] + # RubyVM::InstructionSequence.new calls rb_io_path, which dups the string + # and can leak memory if the dup raises + assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true) + MyError = Class.new(StandardError) + String.prepend(Module.new do + def initialize_dup(_) + if $raise_on_dup + raise MyError + else + super + end + end + end) + + code = proc do |t| + Tempfile.create do |f| + $raise_on_dup = true + t.times do + RubyVM::InstructionSequence.new(f) + rescue MyError + else + raise "MyError was not raised during RubyVM::InstructionSequence.new" + end + ensure + $raise_on_dup = false + end + end + + code.call(100) + begin; + code.call(1_000_000) + end; + end + def test_frozen_string_literal_compile_option $f = 'f' line = __LINE__ + 2 @@ -808,7 +853,7 @@ class TestISeq < Test::Unit::TestCase GC.start Float(30) } - assert_equal :new, r.take + assert_equal :new, r.value RUBY end @@ -859,9 +904,28 @@ class TestISeq < Test::Unit::TestCase end end + def test_serialize_anonymous_outer_variables + iseq = RubyVM::InstructionSequence.compile(<<~'RUBY') + obj = Object.new + def obj.test + [1].each do + raise "Oops" + rescue + return it + end + end + obj + RUBY + + binary = iseq.to_binary # [Bug # 21370] + roundtripped_iseq = RubyVM::InstructionSequence.load_from_binary(binary) + object = roundtripped_iseq.eval + assert_equal 1, object.test + end + def test_loading_kwargs_memory_leak assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~'end;'}", rss: true) - a = iseq_to_binary(RubyVM::InstructionSequence.compile("foo(bar: :baz)")) + a = RubyVM::InstructionSequence.compile("foo(bar: :baz)").to_binary begin; 1_000_000.times do RubyVM::InstructionSequence.load_from_binary(a) diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb index 4563308fa2..c836abd0c6 100644 --- a/test/ruby/test_keyword.rb +++ b/test/ruby/test_keyword.rb @@ -2424,6 +2424,21 @@ class TestKeywordArguments < Test::Unit::TestCase assert_raise(ArgumentError) { m.call(42, a: 1, **h2) } end + def test_ruby2_keywords_post_arg + def self.a(*c, **kw) [c, kw] end + def self.b(*a, b) a(*a, b) end + assert_warn(/Skipping set of ruby2_keywords flag for b \(method accepts keywords or post arguments or method does not accept argument splat\)/) do + assert_nil(singleton_class.send(:ruby2_keywords, :b)) + end + assert_equal([[{foo: 1}, {bar: 1}], {}], b({foo: 1}, bar: 1)) + + b = ->(*a, b){a(*a, b)} + assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc accepts keywords or post arguments or proc does not accept argument splat\)/) do + b.ruby2_keywords + end + assert_equal([[{foo: 1}, {bar: 1}], {}], b.({foo: 1}, bar: 1)) + end + def test_proc_ruby2_keywords h1 = {:a=>1} foo = ->(*args, &block){block.call(*args)} @@ -2436,8 +2451,8 @@ class TestKeywordArguments < Test::Unit::TestCase assert_raise(ArgumentError) { foo.call(:a=>1, &->(arg, **kw){[arg, kw]}) } assert_equal(h1, foo.call(:a=>1, &->(arg){arg})) - [->(){}, ->(arg){}, ->(*args, **kw){}, ->(*args, k: 1){}, ->(*args, k: ){}].each do |pr| - assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc accepts keywords or proc does not accept argument splat\)/) do + [->(){}, ->(arg){}, ->(*args, x){}, ->(*args, **kw){}, ->(*args, k: 1){}, ->(*args, k: ){}].each do |pr| + assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc accepts keywords or post arguments or proc does not accept argument splat\)/) do pr.ruby2_keywords end end @@ -2790,10 +2805,21 @@ class TestKeywordArguments < Test::Unit::TestCase assert_equal(:opt, o.clear_last_opt(a: 1)) assert_nothing_raised(ArgumentError) { o.clear_last_empty_method(a: 1) } - assert_warn(/Skipping set of ruby2_keywords flag for bar \(method accepts keywords or method does not accept argument splat\)/) do + assert_warn(/Skipping set of ruby2_keywords flag for bar \(method accepts keywords or post arguments or method does not accept argument splat\)/) do assert_nil(c.send(:ruby2_keywords, :bar)) end + c.class_eval do + def bar_post(*a, x) = nil + define_method(:bar_post_bmethod) { |*a, x| } + end + assert_warn(/Skipping set of ruby2_keywords flag for bar_post \(method accepts keywords or post arguments or method does not accept argument splat\)/) do + assert_nil(c.send(:ruby2_keywords, :bar_post)) + end + assert_warn(/Skipping set of ruby2_keywords flag for bar_post_bmethod \(method accepts keywords or post arguments or method does not accept argument splat\)/) do + assert_nil(c.send(:ruby2_keywords, :bar_post_bmethod)) + end + utf16_sym = "abcdef".encode("UTF-16LE").to_sym c.send(:define_method, utf16_sym, c.instance_method(:itself)) assert_warn(/abcdef/) do @@ -4033,7 +4059,7 @@ class TestKeywordArguments < Test::Unit::TestCase tap { m } GC.start tap { m } - }, bug8964 + }, bug8964, timeout: 30 assert_normal_exit %q{ prc = Proc.new {|a: []|} GC.stress = true diff --git a/test/ruby/test_lambda.rb b/test/ruby/test_lambda.rb index 3cbb54306c..c1858a36dd 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,7 +276,7 @@ class TestLambdaParameters < Test::Unit::TestCase end def test_do_lambda_source_location - exp = [__LINE__ + 1, 12, __LINE__ + 5, 7] + exp = [__LINE__ + 1, 10, __LINE__ + 5, 7] lmd = ->(x, y, z) do @@ -288,7 +288,7 @@ class TestLambdaParameters < Test::Unit::TestCase end def test_brace_lambda_source_location - exp = [__LINE__ + 1, 12, __LINE__ + 5, 5] + exp = [__LINE__ + 1, 10, __LINE__ + 5, 5] lmd = ->(x, y, z) { 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_m17n.rb b/test/ruby/test_m17n.rb index b0e2e9f849..9f7a3c7f4b 100644 --- a/test/ruby/test_m17n.rb +++ b/test/ruby/test_m17n.rb @@ -186,33 +186,35 @@ class TestM17N < Test::Unit::TestCase end def test_string_inspect_encoding - EnvUtil.suppress_warning do - begin - orig_int = Encoding.default_internal - orig_ext = Encoding.default_external - Encoding.default_internal = nil - [Encoding::UTF_8, Encoding::EUC_JP, Encoding::Windows_31J, Encoding::GB18030]. - each do |e| - Encoding.default_external = e - str = "\x81\x30\x81\x30".force_encoding('GB18030') - assert_equal(Encoding::GB18030 == e ? %{"#{str}"} : '"\x{81308130}"', str.inspect) - str = e("\xa1\x8f\xa1\xa1") - expected = "\"\\xA1\x8F\xA1\xA1\"".force_encoding("EUC-JP") - assert_equal(Encoding::EUC_JP == e ? expected : "\"\\xA1\\x{8FA1A1}\"", str.inspect) - str = s("\x81@") - assert_equal(Encoding::Windows_31J == e ? %{"#{str}"} : '"\x{8140}"', str.inspect) - str = "\u3042\u{10FFFD}" - assert_equal(Encoding::UTF_8 == e ? %{"#{str}"} : '"\u3042\u{10FFFD}"', str.inspect) - end - Encoding.default_external = Encoding::UTF_8 - [Encoding::UTF_16BE, Encoding::UTF_16LE, Encoding::UTF_32BE, Encoding::UTF_32LE, - Encoding::UTF8_SOFTBANK].each do |e| - str = "abc".encode(e) - assert_equal('"abc"', str.inspect) - end - ensure - Encoding.default_internal = orig_int - Encoding.default_external = orig_ext + [ + Encoding::UTF_8, + Encoding::EUC_JP, + Encoding::Windows_31J, + Encoding::GB18030, + ].each do |e| + EnvUtil.with_default_external(e) do + str = "\x81\x30\x81\x30".force_encoding('GB18030') + assert_equal(Encoding::GB18030 == e ? %{"#{str}"} : '"\x{81308130}"', str.inspect) + str = e("\xa1\x8f\xa1\xa1") + expected = "\"\\xA1\x8F\xA1\xA1\"".force_encoding("EUC-JP") + assert_equal(Encoding::EUC_JP == e ? expected : "\"\\xA1\\x{8FA1A1}\"", str.inspect) + str = s("\x81@") + assert_equal(Encoding::Windows_31J == e ? %{"#{str}"} : '"\x{8140}"', str.inspect) + str = "\u3042\u{10FFFD}" + assert_equal(Encoding::UTF_8 == e ? %{"#{str}"} : '"\u3042\u{10FFFD}"', str.inspect) + end + end + + EnvUtil.with_default_external(Encoding::UTF_8) do + [ + Encoding::UTF_16BE, + Encoding::UTF_16LE, + Encoding::UTF_32BE, + Encoding::UTF_32LE, + Encoding::UTF8_SOFTBANK + ].each do |e| + str = "abc".encode(e) + assert_equal('"abc"', str.inspect) end end end @@ -246,59 +248,43 @@ class TestM17N < Test::Unit::TestCase end def test_object_utf16_32_inspect - EnvUtil.suppress_warning do - begin - orig_int = Encoding.default_internal - orig_ext = Encoding.default_external - Encoding.default_internal = nil - Encoding.default_external = Encoding::UTF_8 - o = Object.new - [Encoding::UTF_16BE, Encoding::UTF_16LE, Encoding::UTF_32BE, Encoding::UTF_32LE].each do |e| - o.instance_eval "undef inspect;def inspect;'abc'.encode('#{e}');end" - assert_equal '[abc]', [o].inspect - end - ensure - Encoding.default_internal = orig_int - Encoding.default_external = orig_ext + EnvUtil.with_default_external(Encoding::UTF_8) do + o = Object.new + [Encoding::UTF_16BE, Encoding::UTF_16LE, Encoding::UTF_32BE, Encoding::UTF_32LE].each do |e| + o.instance_eval "undef inspect;def inspect;'abc'.encode('#{e}');end" + assert_equal '[abc]', [o].inspect end end end def test_object_inspect_external - orig_v, $VERBOSE = $VERBOSE, false - orig_int, Encoding.default_internal = Encoding.default_internal, nil - orig_ext = Encoding.default_external - omit "https://bugs.ruby-lang.org/issues/18338" o = Object.new - Encoding.default_external = Encoding::UTF_16BE - def o.inspect - "abc" - end - assert_nothing_raised(Encoding::CompatibilityError) { [o].inspect } + EnvUtil.with_default_external(Encoding::UTF_16BE) do + def o.inspect + "abc" + end + assert_nothing_raised(Encoding::CompatibilityError) { [o].inspect } - def o.inspect - "abc".encode(Encoding.default_external) + def o.inspect + "abc".encode(Encoding.default_external) + end + assert_equal '[abc]', [o].inspect end - assert_equal '[abc]', [o].inspect - - Encoding.default_external = Encoding::US_ASCII - def o.inspect - "\u3042" - end - assert_equal '[\u3042]', [o].inspect + EnvUtil.with_default_external(Encoding::US_ASCII) do + def o.inspect + "\u3042" + end + assert_equal '[\u3042]', [o].inspect - def o.inspect - "\x82\xa0".force_encoding(Encoding::Windows_31J) + def o.inspect + "\x82\xa0".force_encoding(Encoding::Windows_31J) + end + assert_equal '[\x{82A0}]', [o].inspect end - assert_equal '[\x{82A0}]', [o].inspect - ensure - Encoding.default_internal = orig_int - Encoding.default_external = orig_ext - $VERBOSE = orig_v end def test_str_dump diff --git a/test/ruby/test_marshal.rb b/test/ruby/test_marshal.rb index 2aa2a38f80..eb66994801 100644 --- a/test/ruby/test_marshal.rb +++ b/test/ruby/test_marshal.rb @@ -268,7 +268,11 @@ class TestMarshal < Test::Unit::TestCase classISO8859_1.name ClassISO8859_1 = classISO8859_1 - def test_class_nonascii + moduleUTF8 = const_set("C\u{30af 30e9 30b9}", Module.new) + moduleUTF8.name + ModuleUTF8 = moduleUTF8 + + def test_nonascii_class_instance a = ClassUTF8.new assert_instance_of(ClassUTF8, Marshal.load(Marshal.dump(a)), '[ruby-core:24790]') @@ -301,6 +305,12 @@ class TestMarshal < Test::Unit::TestCase end end + def test_nonascii_class_module + assert_same(ClassUTF8, Marshal.load(Marshal.dump(ClassUTF8))) + assert_same(ClassISO8859_1, Marshal.load(Marshal.dump(ClassISO8859_1))) + assert_same(ModuleUTF8, Marshal.load(Marshal.dump(ModuleUTF8))) + end + def test_regexp2 assert_equal(/\\u/, Marshal.load("\004\b/\b\\\\u\000")) assert_equal(/u/, Marshal.load("\004\b/\a\\u\000")) @@ -459,6 +469,30 @@ class TestMarshal < Test::Unit::TestCase assert_equal(o1.foo, o2.foo) end + class TooComplex + def initialize + @marshal_too_complex = 1 + end + end + + def test_complex_shape_object_id_not_dumped + if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) + assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS + end + 8.times do |i| + TooComplex.new.instance_variable_set("@TestObjectIdTooComplex#{i}", 1) + end + obj = TooComplex.new + ivar = "@a#{rand(10_000).to_s.rjust(5, '0')}" + obj.instance_variable_set(ivar, 1) + + if defined?(RubyVM::Shape) + assert_predicate(RubyVM::Shape.of(obj), :too_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) + end + def test_marshal_complex assert_raise(ArgumentError){Marshal.load("\x04\bU:\fComplex[\x05")} assert_raise(ArgumentError){Marshal.load("\x04\bU:\fComplex[\x06i\x00")} diff --git a/test/ruby/test_math.rb b/test/ruby/test_math.rb index 6e67099c6b..e134600cc4 100644 --- a/test/ruby/test_math.rb +++ b/test/ruby/test_math.rb @@ -147,6 +147,13 @@ class TestMath < Test::Unit::TestCase check(Math::E ** 2, Math.exp(2)) end + def test_expm1 + check(0, Math.expm1(0)) + check(Math.sqrt(Math::E) - 1, Math.expm1(0.5)) + check(Math::E - 1, Math.expm1(1)) + check(Math::E ** 2 - 1, Math.expm1(2)) + end + def test_log check(0, Math.log(1)) check(1, Math.log(Math::E)) @@ -201,6 +208,19 @@ class TestMath < Test::Unit::TestCase assert_nothing_raised { assert_infinity(-Math.log10(0)) } end + def test_log1p + check(0, Math.log1p(0)) + check(1, Math.log1p(Math::E - 1)) + check(Math.log(2.0 ** 64 + 1), Math.log1p(1 << 64)) + check(Math.log(2) * 1024.0, Math.log1p(2 ** 1024)) + assert_nothing_raised { assert_infinity(Math.log1p(1.0/0)) } + assert_nothing_raised { assert_infinity(-Math.log1p(-1.0)) } + assert_raise_with_message(Math::DomainError, /\blog1p\b/) { Math.log1p(-1.1) } + assert_raise_with_message(Math::DomainError, /\blog1p\b/) { Math.log1p(-Float::EPSILON-1) } + assert_nothing_raised { assert_nan(Math.log1p(Float::NAN)) } + assert_nothing_raised { assert_infinity(-Math.log1p(-1)) } + end + def test_sqrt check(0, Math.sqrt(0)) check(1, Math.sqrt(1)) @@ -301,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_memory_view.rb b/test/ruby/test_memory_view.rb index 5a39084d18..d0122ddd59 100644 --- a/test/ruby/test_memory_view.rb +++ b/test/ruby/test_memory_view.rb @@ -335,7 +335,7 @@ class TestMemoryView < Test::Unit::TestCase p mv[[0, 2]] mv[[1, 3]] end - p r.take + p r.value end; end end diff --git a/test/ruby/test_method.rb b/test/ruby/test_method.rb index a865f6100b..8561f841a8 100644 --- a/test/ruby/test_method.rb +++ b/test/ruby/test_method.rb @@ -284,8 +284,10 @@ class TestMethod < Test::Unit::TestCase assert_raise(TypeError) { m.bind(Object.new) } cx = EnvUtil.labeled_class("X\u{1f431}") - assert_raise_with_message(TypeError, /X\u{1f431}/) do - o.method(cx) + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(TypeError, /X\u{1f431}/) do + o.method(cx) + end end end @@ -315,9 +317,12 @@ class TestMethod < Test::Unit::TestCase assert_raise(TypeError) do Class.new.class_eval { define_method(:bar, o.method(:bar)) } end + cx = EnvUtil.labeled_class("X\u{1f431}") - assert_raise_with_message(TypeError, /X\u{1F431}/) do - Class.new {define_method(cx) {}} + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(TypeError, /X\u{1F431}/) do + Class.new {define_method(cx) {}} + end end end @@ -1612,7 +1617,7 @@ class TestMethod < Test::Unit::TestCase begin foo(1) rescue ArgumentError => e - assert_equal "main.rb:#{$line_method}:in 'foo'", e.backtrace.first + assert_equal "main.rb:#{$line_method}:in 'Object#foo'", e.backtrace.first end EOS END_OF_BODY diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index 9a21113fe0..9ed6c1e321 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 @@ -1291,8 +1273,11 @@ class TestModule < Test::Unit::TestCase assert_raise(NameError) { c1.const_set("X\u{3042}".encode("utf-16le"), :foo) } assert_raise(NameError) { c1.const_set("X\u{3042}".encode("utf-32be"), :foo) } assert_raise(NameError) { c1.const_set("X\u{3042}".encode("utf-32le"), :foo) } + cx = EnvUtil.labeled_class("X\u{3042}") - assert_raise_with_message(TypeError, /X\u{3042}/) { c1.const_set(cx, :foo) } + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(TypeError, /X\u{3042}/) { c1.const_set(cx, :foo) } + end end def test_const_get_invalid_name @@ -1449,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 }) @@ -1457,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 @@ -3020,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 @@ -3077,7 +3073,7 @@ class TestModule < Test::Unit::TestCase end def test_prepend_gc - assert_separately [], %{ + assert_ruby_status [], %{ module Foo end class Object @@ -3371,11 +3367,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) @@ -3383,14 +3379,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_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 ab492743f6..35496ac875 100644 --- a/test/ruby/test_numeric.rb +++ b/test/ruby/test_numeric.rb @@ -18,18 +18,24 @@ class TestNumeric < Test::Unit::TestCase assert_raise_with_message(TypeError, /can't be coerced into /) {1|:foo} assert_raise_with_message(TypeError, /can't be coerced into /) {1^:foo} - assert_raise_with_message(TypeError, /:\u{3042}/) {1+:"\u{3042}"} - assert_raise_with_message(TypeError, /:\u{3042}/) {1&:"\u{3042}"} - assert_raise_with_message(TypeError, /:\u{3042}/) {1|:"\u{3042}"} - assert_raise_with_message(TypeError, /:\u{3042}/) {1^:"\u{3042}"} - assert_raise_with_message(TypeError, /:"\\u3042"/) {1+:"\u{3042}"} - assert_raise_with_message(TypeError, /:"\\u3042"/) {1&:"\u{3042}"} - assert_raise_with_message(TypeError, /:"\\u3042"/) {1|:"\u{3042}"} - assert_raise_with_message(TypeError, /:"\\u3042"/) {1^:"\u{3042}"} - assert_raise_with_message(TypeError, /:\u{3044}/) {1+"\u{3044}".to_sym} - assert_raise_with_message(TypeError, /:\u{3044}/) {1&"\u{3044}".to_sym} - assert_raise_with_message(TypeError, /:\u{3044}/) {1|"\u{3044}".to_sym} - assert_raise_with_message(TypeError, /:\u{3044}/) {1^"\u{3044}".to_sym} + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(TypeError, /:\u{3042}/) {1+:"\u{3042}"} + assert_raise_with_message(TypeError, /:\u{3042}/) {1&:"\u{3042}"} + assert_raise_with_message(TypeError, /:\u{3042}/) {1|:"\u{3042}"} + assert_raise_with_message(TypeError, /:\u{3042}/) {1^:"\u{3042}"} + + assert_raise_with_message(TypeError, /:\u{3044}/) {1+"\u{3044}".to_sym} + assert_raise_with_message(TypeError, /:\u{3044}/) {1&"\u{3044}".to_sym} + assert_raise_with_message(TypeError, /:\u{3044}/) {1|"\u{3044}".to_sym} + assert_raise_with_message(TypeError, /:\u{3044}/) {1^"\u{3044}".to_sym} + end + + EnvUtil.with_default_internal(Encoding::US_ASCII) do + assert_raise_with_message(TypeError, /:"\\u3042"/) {1+:"\u{3042}"} + assert_raise_with_message(TypeError, /:"\\u3042"/) {1&:"\u{3042}"} + assert_raise_with_message(TypeError, /:"\\u3042"/) {1|:"\u{3042}"} + assert_raise_with_message(TypeError, /:"\\u3042"/) {1^:"\u{3042}"} + end bug10711 = '[ruby-core:67405] [Bug #10711]' exp = "1.2 can't be coerced into Integer" @@ -483,6 +489,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 7d00422629..f4dfe2251b 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,41 @@ 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 + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + c = Class.new do + attr_reader :a, :b, :c - 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' + def initialize + @a = nil + @b = nil + @c = nil + end + end - 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) + 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) + end; end def test_convert_string @@ -950,6 +953,19 @@ class TestObject < Test::Unit::TestCase assert_match(/\bInspect\u{3042}:.* @\u{3044}=42\b/, x.inspect) x.instance_variable_set("@\u{3046}".encode(Encoding::EUC_JP), 6) assert_match(/@\u{3046}=6\b/, x.inspect) + + x = Object.new + x.singleton_class.class_eval do + private def instance_variables_to_inspect = [:@host, :@user] + end + + x.instance_variable_set(:@host, "localhost") + x.instance_variable_set(:@user, "root") + x.instance_variable_set(:@password, "hunter2") + s = x.inspect + assert_include(s, "@host=\"localhost\"") + assert_include(s, "@user=\"root\"") + assert_not_include(s, "@password=") end def test_singleton_methods diff --git a/test/ruby/test_object_id.rb b/test/ruby/test_object_id.rb new file mode 100644 index 0000000000..adb819febc --- /dev/null +++ b/test/ruby/test_object_id.rb @@ -0,0 +1,303 @@ +require 'test/unit' +require "securerandom" + +class TestObjectId < Test::Unit::TestCase + def setup + @obj = Object.new + end + + def test_dup_new_id + id = @obj.object_id + refute_equal id, @obj.dup.object_id + end + + def test_dup_with_ivar_and_id + id = @obj.object_id + @obj.instance_variable_set(:@foo, 42) + + copy = @obj.dup + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + end + + def test_dup_with_id_and_ivar + @obj.instance_variable_set(:@foo, 42) + id = @obj.object_id + + copy = @obj.dup + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + end + + def test_dup_with_id_and_ivar_and_frozen + @obj.instance_variable_set(:@foo, 42) + @obj.freeze + id = @obj.object_id + + copy = @obj.dup + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + refute_predicate copy, :frozen? + end + + def test_clone_new_id + id = @obj.object_id + refute_equal id, @obj.clone.object_id + end + + def test_clone_with_ivar_and_id + id = @obj.object_id + @obj.instance_variable_set(:@foo, 42) + + copy = @obj.clone + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + end + + def test_clone_with_id_and_ivar + @obj.instance_variable_set(:@foo, 42) + id = @obj.object_id + + copy = @obj.clone + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + end + + def test_clone_with_id_and_ivar_and_frozen + @obj.instance_variable_set(:@foo, 42) + @obj.freeze + id = @obj.object_id + + copy = @obj.clone + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + assert_predicate copy, :frozen? + end + + def test_marshal_new_id + return pass if @obj.is_a?(Module) + + id = @obj.object_id + refute_equal id, Marshal.load(Marshal.dump(@obj)).object_id + end + + def test_marshal_with_ivar_and_id + return pass if @obj.is_a?(Module) + + id = @obj.object_id + @obj.instance_variable_set(:@foo, 42) + + copy = Marshal.load(Marshal.dump(@obj)) + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + end + + def test_marshal_with_id_and_ivar + return pass if @obj.is_a?(Module) + + @obj.instance_variable_set(:@foo, 42) + id = @obj.object_id + + copy = Marshal.load(Marshal.dump(@obj)) + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + end + + def test_marshal_with_id_and_ivar_and_frozen + return pass if @obj.is_a?(Module) + + @obj.instance_variable_set(:@foo, 42) + @obj.freeze + id = @obj.object_id + + copy = Marshal.load(Marshal.dump(@obj)) + refute_equal id, copy.object_id + assert_equal 42, copy.instance_variable_get(:@foo) + refute_predicate copy, :frozen? + end + + def test_object_id_need_resize + (3 - @obj.instance_variables.size).times do |i| + @obj.instance_variable_set("@a_#{i}", "[Bug #21445]") + end + @obj.object_id + GC.start + end +end + +class TestObjectIdClass < TestObjectId + def setup + @obj = Class.new + end +end + +class TestObjectIdGeneric < TestObjectId + def setup + @obj = Array.new + end +end + +class TestObjectIdTooComplex < TestObjectId + class TooComplex + def initialize + @too_complex_obj_id_test = 1 + end + end + + def setup + if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) + assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS + end + 8.times do |i| + TooComplex.new.instance_variable_set("@TestObjectIdTooComplex#{i}", 1) + end + @obj = TooComplex.new + @obj.instance_variable_set("@a#{rand(10_000)}", 1) + + if defined?(RubyVM::Shape) + assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + end + end +end + +class TestObjectIdTooComplexClass < TestObjectId + class TooComplex < Module + end + + def setup + if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) + assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS + end + + @obj = TooComplex.new + + @obj.instance_variable_set("@___#{SecureRandom.hex}", 1) + + 8.times do |i| + @obj.instance_variable_set("@TestObjectIdTooComplexClass#{i}", 1) + @obj.remove_instance_variable("@TestObjectIdTooComplexClass#{i}") + end + + @obj.instance_variable_set("@test", 1) + + if defined?(RubyVM::Shape) + assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + end + end +end + +class TestObjectIdTooComplexGeneric < TestObjectId + class TooComplex < Array + end + + def setup + if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) + assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS + end + 8.times do |i| + TooComplex.new.instance_variable_set("@TestObjectIdTooComplexGeneric#{i}", 1) + end + @obj = TooComplex.new + @obj.instance_variable_set("@a#{rand(10_000)}", 1) + @obj.instance_variable_set("@a#{rand(10_000)}", 1) + + if defined?(RubyVM::Shape) + assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + end + end +end + +class TestObjectIdRactor < Test::Unit::TestCase + def test_object_id_race_free + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + Warning[:experimental] = false + class MyClass + attr_reader :a, :b, :c + def initialize + @a = @b = @c = nil + end + end + N = 10_000 + objs = Ractor.make_shareable(N.times.map { MyClass.new }) + results = 4.times.map{ + Ractor.new(objs) { |objs| + vars = [] + ids = [] + objs.each do |obj| + vars << obj.a << obj.b << obj.c + ids << obj.object_id + end + [vars, ids] + } + }.map(&:value) + assert_equal 1, results.uniq.size + end; + end + + def test_external_object_id_ractor_move + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + Warning[:experimental] = false + class MyClass + attr_reader :a, :b, :c + def initialize + @a = @b = @c = nil + end + end + obj = Ractor.make_shareable(MyClass.new) + object_id = obj.object_id + obj = Ractor.new { Ractor.receive }.send(obj, move: true).value + assert_equal object_id, obj.object_id + end; + end +end + +class TestObjectIdStruct < TestObjectId + EmbeddedStruct = Struct.new(:embedded_field) + + def setup + @obj = EmbeddedStruct.new + end +end + +class TestObjectIdStructGenIvar < TestObjectId + GenIvarStruct = Struct.new(:a, :b, :c) + + def setup + @obj = GenIvarStruct.new + end +end + +class TestObjectIdStructNotEmbed < TestObjectId + MANY_IVS = 80 + + StructNotEmbed = Struct.new(*MANY_IVS.times.map { |i| :"field_#{i}" }) + + def setup + @obj = StructNotEmbed.new + end +end + +class TestObjectIdStructTooComplex < TestObjectId + StructTooComplex = Struct.new(:a) do + def initialize + @too_complex_obj_id_test = 1 + end + end + + def setup + if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) + assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS + end + 8.times do |i| + StructTooComplex.new.instance_variable_set("@TestObjectIdStructTooComplex#{i}", 1) + end + @obj = StructTooComplex.new + @obj.instance_variable_set("@a#{rand(10_000)}", 1) + + if defined?(RubyVM::Shape) + assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + end + end +end diff --git a/test/ruby/test_objectspace.rb b/test/ruby/test_objectspace.rb index d161d8079a..a479547599 100644 --- a/test/ruby/test_objectspace.rb +++ b/test/ruby/test_objectspace.rb @@ -8,7 +8,7 @@ class TestObjectSpace < Test::Unit::TestCase line = $1.to_i code = <<"End" define_method("test_id2ref_#{line}") {\ - o = ObjectSpace._id2ref(obj.object_id);\ + o = EnvUtil.suppress_warning { ObjectSpace._id2ref(obj.object_id) } assert_same(obj, o, "didn't round trip: \#{obj.inspect}");\ } End @@ -57,12 +57,12 @@ End def test_id2ref_invalid_argument msg = /no implicit conversion/ - assert_raise_with_message(TypeError, msg) {ObjectSpace._id2ref(nil)} - assert_raise_with_message(TypeError, msg) {ObjectSpace._id2ref(false)} - assert_raise_with_message(TypeError, msg) {ObjectSpace._id2ref(true)} - assert_raise_with_message(TypeError, msg) {ObjectSpace._id2ref(:a)} - assert_raise_with_message(TypeError, msg) {ObjectSpace._id2ref("0")} - assert_raise_with_message(TypeError, msg) {ObjectSpace._id2ref(Object.new)} + assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(nil) } } + assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(false) } } + assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(true) } } + assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(:a) } } + assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref("0") } } + assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(Object.new) } } end def test_id2ref_invalid_symbol_id @@ -70,7 +70,7 @@ End # 8 bits of the object is equal to RUBY_SYMBOL_FLAG, so we need to make # sure that the bottom 8 bits remain unchanged. msg = /is not a symbol id value/ - assert_raise_with_message(RangeError, msg) { ObjectSpace._id2ref(:a.object_id + 256) } + assert_raise_with_message(RangeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(:a.object_id + 256) } } end def test_count_objects @@ -94,7 +94,7 @@ End end def test_finalizer - assert_in_out_err(["-e", <<-END], "", %w(:ok :ok :ok :ok), []) + assert_in_out_err(["-e", <<-END], "", %w(:ok :ok :ok), []) a = [] ObjectSpace.define_finalizer(a) { p :ok } b = a.dup @@ -137,6 +137,25 @@ End } end + def test_finalizer_copy + assert_in_out_err(["-e", <<~'RUBY'], "", %w(:ok), []) + def fin + ids = Set.new + ->(id) { puts "object_id (#{id}) reused" unless ids.add?(id) } + end + + OBJ = Object.new + ObjectSpace.define_finalizer(OBJ, fin) + OBJ.freeze + + 10.times do + OBJ.clone + end + + p :ok + RUBY + end + def test_finalizer_with_super assert_in_out_err(["-e", <<-END], "", %w(:ok), []) class A @@ -265,6 +284,21 @@ End end; end + def test_id2ref_table_build + assert_separately([], <<-End) + 10.times do + Object.new.object_id + end + + GC.start(immediate_mark: false) + + obj = Object.new + EnvUtil.suppress_warning do + assert_equal obj, ObjectSpace._id2ref(obj.object_id) + end + End + end + def test_each_object_singleton_class assert_separately([], <<-End) class C diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index e39eafa5e5..5d16984eef 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 @@ -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_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 35aa16063d..959ea87f25 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -1637,6 +1637,10 @@ class TestProc < Test::Unit::TestCase assert_equal(3, b.local_variable_get(:when)) assert_equal(4, b.local_variable_get(:begin)) assert_equal(5, b.local_variable_get(:end)) + + assert_raise_with_message(NameError, /local variable \Wdefault\W/) { + binding.local_variable_get(:default) + } end def test_local_variable_set @@ -1651,33 +1655,103 @@ class TestProc < Test::Unit::TestCase def test_numparam_is_not_local_variables "foo".tap do - _9 + _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) } "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 + _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 @@ -1686,32 +1760,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 2d9d1416aa..b3a88b664c 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. @@ -114,14 +116,19 @@ class TestProcess < Test::Unit::TestCase } assert_raise(ArgumentError) { Process.getrlimit(:FOO) } assert_raise(ArgumentError) { Process.getrlimit("FOO") } - assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.getrlimit("\u{30eb 30d3 30fc}") } + + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.getrlimit("\u{30eb 30d3 30fc}") } + end end def test_rlimit_value return unless rlimit_exist? assert_raise(ArgumentError) { Process.setrlimit(:FOO, 0) } assert_raise(ArgumentError) { Process.setrlimit(:CORE, :FOO) } - assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.setrlimit("\u{30eb 30d3 30fc}", 0) } + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.setrlimit("\u{30eb 30d3 30fc}", 0) } + end assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.setrlimit(:CORE, "\u{30eb 30d3 30fc}") } with_tmpchdir do s = run_in_child(<<-'End') @@ -275,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) @@ -1682,9 +1690,10 @@ class TestProcess < Test::Unit::TestCase if u = Etc.getpwuid(Process.uid) assert_equal(Process.uid, Process::UID.from_name(u.name), u.name) end - assert_raise_with_message(ArgumentError, /\u{4e0d 5b58 5728}/) { + exc = assert_raise_kind_of(ArgumentError, SystemCallError) { Process::UID.from_name("\u{4e0d 5b58 5728}") } + assert_match(/\u{4e0d 5b58 5728}/, exc.message) if exc.is_a?(ArgumentError) end end @@ -1987,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 @@ -2378,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 @@ -2769,7 +2778,9 @@ EOS Process.warmup - assert_equal(total_slots_before, GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_slots)) + # TODO: flaky + # assert_equal(total_slots_before, GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_slots)) + assert_equal(0, GC.stat(:heap_empty_pages)) assert_operator(GC.stat(:total_freed_pages), :>, 0) end; diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index ec94df361f..6ae511217a 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,15 @@ 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_shareability_error_uses_inspect + x = (+"").instance_exec { method(:to_s) } + def x.to_s + raise "this should not be called" + end + assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()> because it refers unshareable objects", exception: Ractor::Error) end def test_default_thread_group @@ -66,11 +56,163 @@ class TestRactor < Test::Unit::TestCase Warning[:experimental] = false main_ractor_id = Thread.current.group.object_id - ractor_id = Ractor.new { Thread.current.group.object_id }.take + ractor_id = Ractor.new { Thread.current.group.object_id }.value refute_equal main_ractor_id, ractor_id end; end + def test_class_instance_variables + assert_ractor(<<~'RUBY') + # Once we're in multi-ractor mode, the codepaths + # for class instance variables are a bit different. + Ractor.new {}.value + + class TestClass + @a = 1 + @b = 2 + @c = 3 + @d = 4 + end + + assert_equal 4, TestClass.remove_instance_variable(:@d) + assert_nil TestClass.instance_variable_get(:@d) + assert_equal 4, TestClass.instance_variable_set(:@d, 4) + assert_equal 4, TestClass.instance_variable_get(:@d) + RUBY + end + + def test_struct_instance_variables + assert_ractor(<<~'RUBY') + StructIvar = Struct.new(:member) do + def initialize(*) + super + @ivar = "ivar" + end + attr_reader :ivar + end + obj = StructIvar.new("member") + obj_copy = Ractor.new { Ractor.receive }.send(obj).value + assert_equal obj.ivar, obj_copy.ivar + refute_same obj.ivar, obj_copy.ivar + assert_equal obj.member, obj_copy.member + refute_same obj.member, obj_copy.member + RUBY + end + + def test_move_nested_hash_during_gc_with_yjit + assert_ractor(<<~'RUBY', 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 + Process.fork + rescue Ractor::IsolationError => e + e + end + assert_equal Ractor::IsolationError, ractor.value.class + RUBY + end if Process.respond_to?(:fork) + + def test_require_raises_and_no_ractor_belonging_issue + assert_ractor(<<~'RUBY') + require "tempfile" + f = Tempfile.new(["file_to_require_from_ractor", ".rb"]) + f.write("raise 'uh oh'") + f.flush + err_msg = Ractor.new(f.path) do |path| + begin + require path + rescue RuntimeError => e + e.message # had confirm belonging issue here + else + nil + end + end.value + assert_equal "uh oh", err_msg + RUBY + end + + def test_require_non_string + assert_ractor(<<~'RUBY') + require "tempfile" + require "pathname" + f = Tempfile.new(["file_to_require_from_ractor", ".rb"]) + f.write("") + f.flush + result = Ractor.new(f.path) do |path| + require Pathname.new(path) + "success" + end.value + assert_equal "success", result + RUBY + end + + # [Bug #21398] + def test_port_receive_dnt_with_port_send + omit 'unstable on windows and macos-14' if RUBY_PLATFORM =~ /mswin|mingw|darwin/ + assert_ractor(<<~'RUBY', timeout: 90) + THREADS = 10 + JOBS_PER_THREAD = 50 + ARRAY_SIZE = 20_000 + def ractor_job(job_count, array_size) + port = Ractor::Port.new + workers = (1..4).map do |i| + Ractor.new(port) do |job_port| + while job = Ractor.receive + result = job.map { |x| x * 2 }.sum + job_port.send result + end + end + end + jobs = Array.new(job_count) { Array.new(array_size) { rand(1000) } } + jobs.each_with_index do |job, i| + w_idx = i % 4 + workers[w_idx].send(job) + end + results = [] + jobs.size.times do + result = port.receive # dnt receive + results << result + end + results + end + threads = [] + # creates 40 ractors (THREADSx4) + THREADS.times do + threads << Thread.new do + ractor_job(JOBS_PER_THREAD, ARRAY_SIZE) + end + end + threads.each(&:join) + RUBY + end + + # [Bug #20146] + def test_max_cpu_1 + assert_ractor(<<~'RUBY', args: [{ "RUBY_MAX_CPU" => "1" }]) + assert_equal :ok, Ractor.new { :ok }.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 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 89bb7b20a8..e0edbde463 100644 --- a/test/ruby/test_rational.rb +++ b/test/ruby/test_rational.rb @@ -117,9 +117,13 @@ class Rational_Test < Test::Unit::TestCase assert_equal(Rational(111, 1000), Rational('1.11e-1')) assert_raise(TypeError){Rational(nil)} assert_raise(ArgumentError){Rational('')} - assert_raise_with_message(ArgumentError, /\u{221a 2668}/) { - Rational("\u{221a 2668}") - } + + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(ArgumentError, /\u{221a 2668}/) { + Rational("\u{221a 2668}") + } + end + assert_warning('') { assert_predicate(Rational('1e-99999999999999999999'), :zero?) } diff --git a/test/ruby/test_refinement.rb b/test/ruby/test_refinement.rb index 6ce434790b..209e55294b 100644 --- a/test/ruby/test_refinement.rb +++ b/test/ruby/test_refinement.rb @@ -1933,6 +1933,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 @@ -2712,6 +2735,30 @@ 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 + private def eval_using(mod, s) diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb index 78269f8e9a..9feababa53 100644 --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -1036,10 +1036,12 @@ class TestRegexp < Test::Unit::TestCase [Encoding::UTF_8, Encoding::Shift_JIS, Encoding::EUC_JP].each do |enc| idx = key.encode(enc) pat = /#{idx}/ - test.call {|m| assert_raise_with_message(IndexError, pat, bug10877) {m[idx]} } - test.call {|m| assert_raise_with_message(IndexError, pat, bug18160) {m.offset(idx)} } - test.call {|m| assert_raise_with_message(IndexError, pat, bug18160) {m.begin(idx)} } - test.call {|m| assert_raise_with_message(IndexError, pat, bug18160) {m.end(idx)} } + EnvUtil.with_default_internal(enc) do + test.call {|m| assert_raise_with_message(IndexError, pat, bug10877) {m[idx]} } + test.call {|m| assert_raise_with_message(IndexError, pat, bug18160) {m.offset(idx)} } + test.call {|m| assert_raise_with_message(IndexError, pat, bug18160) {m.begin(idx)} } + test.call {|m| assert_raise_with_message(IndexError, pat, bug18160) {m.end(idx)} } + end end test.call {|m| assert_equal(/a/, m.regexp) } test.call {|m| assert_equal("abc", m.string) } @@ -1308,6 +1310,9 @@ class TestRegexp < Test::Unit::TestCase assert_match(/\A[[:space:]]+\z/, "\r\n\v\f\r\s\u0085") assert_match(/\A[[:ascii:]]+\z/, "\x00\x7F") assert_no_match(/[[:ascii:]]/, "\x80\xFF") + + assert_match(/[[:word:]]/, "\u{200C}") + assert_match(/[[:word:]]/, "\u{200D}") end def test_cclass_R @@ -1664,6 +1669,30 @@ 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_regexp_popped EnvUtil.suppress_warning do assert_nothing_raised { eval("a = 1; /\#{ a }/; a") } @@ -1738,6 +1767,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]]) @@ -1875,6 +1931,12 @@ class TestRegexp < Test::Unit::TestCase end; end + def test_too_big_number_for_repeat_range + assert_raise_with_message(SyntaxError, /too big number for repeat range/) do + eval(%[/|{1000000}/]) + end + end + # This assertion is for porting x2() tests in testpy.py of Onigmo. def assert_match_at(re, str, positions, msg = nil) re = Regexp.new(re) unless re.is_a?(Regexp) @@ -1979,7 +2041,7 @@ class TestRegexp < Test::Unit::TestCase end def per_instance_redos_test(global_timeout, per_instance_timeout, expected_timeout) - assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}", timeout: 60) global_timeout = #{ EnvUtil.apply_timeout_scale(global_timeout).inspect } per_instance_timeout = #{ (per_instance_timeout ? EnvUtil.apply_timeout_scale(per_instance_timeout) : nil).inspect } expected_timeout = #{ EnvUtil.apply_timeout_scale(expected_timeout).inspect } diff --git a/test/ruby/test_require.rb b/test/ruby/test_require.rb index 13e7076391..0067a49700 100644 --- a/test/ruby/test_require.rb +++ b/test/ruby/test_require.rb @@ -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 d7184f1057..cd2dd5d3ff 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,10 +47,15 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err([], "", [], []) end + # 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| - assert_operator(r.size, :<=, 25) - longer = r[1..-1].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 @@ -174,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]) @@ -203,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 @@ -212,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 @@ -240,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]) @@ -260,6 +267,8 @@ class TestRubyOptions < Test::Unit::TestCase end def test_parser_flag + omit if ENV["RUBYOPT"]&.include?("--parser=") + assert_in_out_err(%w(--parser=prism -e) + ["puts :hi"], "", %w(hi), []) assert_in_out_err(%w(--parser=prism --dump=parsetree -e _=:hi), "", /"hi"/, []) @@ -519,6 +528,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 @@ -785,6 +796,12 @@ class TestRubyOptions < Test::Unit::TestCase unless /mswin|mingw/ =~ RUBY_PLATFORM opts[:rlimit_core] = 0 end + opts[:failed] = proc do |status, message = "", out = ""| + if (sig = status.termsig) && Signal.list["SEGV"] == sig + out = "" + end + Test::Unit::CoreAssertions::FailDesc[status, message] + end ExecOptions = opts.freeze # The regexp list that should match the entire stderr output. @@ -834,20 +851,24 @@ class TestRubyOptions < Test::Unit::TestCase end def assert_segv(args, message=nil, list: SEGVTest::ExpectedStderrList, **opt, &block) - pend "macOS 15 is not working with this assertion" if macos?(15) - # 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 = "" - tests = [//, list] unless block + if !block + tests = [//, list, message] + elsif message + tests = [[], [], message] + end assert_in_out_err(args, test_stdin, *tests, encoding: "ASCII-8BIT", **SEGVTest::ExecOptions, **opt, &block) @@ -860,13 +881,12 @@ class TestRubyOptions < Test::Unit::TestCase def test_segv_loaded_features bug7402 = '[ruby-core:49573]' - status = assert_segv(['-e', "END {#{SEGVTest::KILL_SELF}}", - '-e', 'class Bogus; def to_str; exit true; end; end', - '-e', '$".clear', - '-e', '$".unshift Bogus.new', - '-e', '(p $"; abort) unless $".size == 1', - ]) - assert_not_predicate(status, :success?, "segv but success #{bug7402}") + assert_segv(['-e', "END {#{SEGVTest::KILL_SELF}}", + '-e', 'class Bogus; def to_str; exit true; end; end', + '-e', '$".clear', + '-e', '$".unshift Bogus.new', + '-e', '(p $"; abort) unless $".size == 1', + ], bug7402, success: false) end def test_segv_setproctitle @@ -879,8 +899,6 @@ class TestRubyOptions < Test::Unit::TestCase end def assert_crash_report(path, cmd = nil, &block) - pend "macOS 15 is not working with this assertion" if macos?(15) - Dir.mktmpdir("ruby_crash_report") do |dir| list = SEGVTest::ExpectedStderrList if cmd @@ -934,6 +952,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" @@ -980,7 +1019,7 @@ class TestRubyOptions < Test::Unit::TestCase pid = spawn(EnvUtil.rubybin, :in => s, :out => w) w.close assert_nothing_raised('[ruby-dev:37798]') do - result = EnvUtil.timeout(3) {r.read} + result = EnvUtil.timeout(10) {r.read} end Process.wait pid } @@ -1278,4 +1317,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 2bb7858eb2..70a61aa3b5 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] @@ -130,6 +133,12 @@ class TC_Set < Test::Unit::TestCase assert_equal(Set['a','b','c'], set) set = Set[1,2] + ret = set.replace(Set.new('a'..'c')) + + assert_same(set, ret) + assert_equal(Set['a','b','c'], set) + + set = Set[1,2] assert_raise(ArgumentError) { set.replace(3) } @@ -258,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) @@ -287,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) @@ -316,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) @@ -345,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) @@ -365,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) @@ -681,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 + + result = a ^ b - set2 = Set2[1,2,3,4] - ret2 = set2 ^ [2,4,5,5] - assert_instance_of(Set2, ret2) - assert_equal(Set2[1,3,5], ret2) + assert_equal(original_b, b) + assert_equal(Set[2], result) end def test_eq @@ -775,6 +797,10 @@ class TC_Set < Test::Unit::TestCase ret.each { |s| n += s.size } assert_equal(set.size, n) assert_equal(set, ret.flatten) + + set = Set[2,12,9,11,13,4,10,15,3,8,5,0,1,7,14] + ret = set.divide { |a,b| (a - b).abs == 1 } + assert_equal(2, ret.size) end def test_freeze @@ -829,24 +855,32 @@ class TC_Set < Test::Unit::TestCase def test_inspect set1 = Set[1, 2] - assert_equal('#<Set: {1, 2}>', set1.inspect) + assert_equal('Set[1, 2]', set1.inspect) set2 = Set[Set[0], 1, 2, set1] - assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2}>}>', set2.inspect) + assert_equal('Set[Set[0], 1, 2, Set[1, 2]]', set2.inspect) set1.add(set2) - assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2, #<Set: {...}>}>}>', set2.inspect) + assert_equal('Set[Set[0], 1, 2, Set[1, 2, Set[...]]]', set2.inspect) + + 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 set1 = Set[1, 2] - assert_equal('#<Set: {1, 2}>', set1.to_s) + assert_equal('Set[1, 2]', set1.to_s) set2 = Set[Set[0], 1, 2, set1] - assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2}>}>', set2.to_s) + assert_equal('Set[Set[0], 1, 2, Set[1, 2]]', set2.to_s) set1.add(set2) - assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2, #<Set: {...}>}>}>', set2.to_s) + assert_equal('Set[Set[0], 1, 2, Set[1, 2, Set[...]]]', set2.to_s) end def test_compare_by_identity @@ -896,6 +930,39 @@ class TC_Set < Test::Unit::TestCase end end; end + + def test_larger_sets + set = Set.new + 10_000.times do |i| + set << i + end + set = set.dup + + 10_000.times do |i| + 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 @@ -913,6 +980,36 @@ class TC_Enumerable < Test::Unit::TestCase assert_same set, set.to_set assert_not_same set, set.to_set { |o| o } 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 class TC_Set_Builtin < Test::Unit::TestCase diff --git a/test/ruby/test_settracefunc.rb b/test/ruby/test_settracefunc.rb index 55c07abbea..d3b2441e21 100644 --- a/test/ruby/test_settracefunc.rb +++ b/test/ruby/test_settracefunc.rb @@ -1999,7 +1999,7 @@ CODE TracePoint.new(:c_call, &capture_events).enable{ c.new } - assert_equal [:c_call, :itself, :initialize], events[1] + assert_equal [:c_call, :itself, :initialize], events[0] events.clear o = Class.new{ @@ -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__ @@ -2724,7 +2724,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 +2957,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 0c1d8d424e..67e2c543a3 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -2,6 +2,7 @@ require 'test/unit' require 'objspace' require 'json' +require 'securerandom' # These test the functionality of object shapes class TestShapes < Test::Unit::TestCase @@ -92,15 +93,18 @@ class TestShapes < Test::Unit::TestCase # RubyVM::Shape.of returns new instances of shape objects for # each call. This helper method allows us to define equality for # shapes - def assert_shape_equal(shape1, shape2) - assert_equal(shape1.id, shape2.id) - assert_equal(shape1.parent_id, shape2.parent_id) - assert_equal(shape1.depth, shape2.depth) - assert_equal(shape1.type, shape2.type) + 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}, + ) end - def refute_shape_equal(shape1, shape2) - refute_equal(shape1.id, shape2.id) + 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}, + ) end def test_iv_order_correct_on_complex_objects @@ -146,11 +150,14 @@ class TestShapes < Test::Unit::TestCase def test_too_many_ivs_on_class obj = Class.new - (MANY_IVS + 1).times do + obj.instance_variable_set(:@test_too_many_ivs_on_class, 1) + refute_predicate RubyVM::Shape.of(obj), :too_complex? + + MANY_IVS.times do obj.instance_variable_set(:"@a#{_1}", 1) end - assert_false RubyVM::Shape.of(obj).too_complex? + refute_predicate RubyVM::Shape.of(obj), :too_complex? end def test_removing_when_too_many_ivs_on_class @@ -221,7 +228,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 @@ -332,7 +339,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) @@ -500,7 +507,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 @@ -596,8 +603,8 @@ class TestShapes < Test::Unit::TestCase assert_predicate RubyVM::Shape.of(tc), :too_complex? assert_equal 3, tc.very_unique - assert_equal 3, Ractor.new(tc) { |x| Ractor.yield(x.very_unique) }.take - assert_equal tc.instance_variables.sort, Ractor.new(tc) { |x| Ractor.yield(x.instance_variables) }.take.sort + 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 @@ -622,6 +629,97 @@ class TestShapes < Test::Unit::TestCase end; end + def test_too_complex_and_frozen + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + $VERBOSE = nil + class TooComplex + attr_reader :very_unique + end + + RubyVM::Shape::SHAPE_MAX_VARIATIONS.times do + TooComplex.new.instance_variable_set(:"@unique_#{_1}", Object.new) + end + + tc = TooComplex.new + tc.instance_variable_set(:"@very_unique", 3) + + shape = RubyVM::Shape.of(tc) + assert_predicate shape, :too_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, :shape_frozen? + + assert_equal 3, tc.very_unique + assert_equal 3, Ractor.make_shareable(tc).very_unique + end; + end + + def test_object_id_transition_too_complex + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + obj = Object.new + obj.instance_variable_set(:@a, 1) + RubyVM::Shape.exhaust_shapes + assert_equal obj.object_id, obj.object_id + end; + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class Hi; end + obj = Hi.new + obj.instance_variable_set(:@a, 1) + obj.instance_variable_set(:@b, 2) + old_id = obj.object_id + + RubyVM::Shape.exhaust_shapes + obj.remove_instance_variable(:@a) + + assert_equal old_id, obj.object_id + end; + end + + def test_too_complex_and_frozen_and_object_id + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + $VERBOSE = nil + class TooComplex + attr_reader :very_unique + end + + RubyVM::Shape::SHAPE_MAX_VARIATIONS.times do + TooComplex.new.instance_variable_set(:"@unique_#{_1}", Object.new) + end + + tc = TooComplex.new + tc.instance_variable_set(:"@very_unique", 3) + + shape = RubyVM::Shape.of(tc) + assert_predicate shape, :too_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, :shape_frozen? + refute_predicate frozen_shape, :has_object_id? + + assert_equal tc.object_id, tc.object_id + + id_shape = RubyVM::Shape.of(tc) + refute_equal frozen_shape.id, id_shape.id + assert_predicate id_shape, :too_complex? + assert_predicate id_shape, :has_object_id? + assert_predicate id_shape, :shape_frozen? + + assert_equal 3, tc.very_unique + assert_equal 3, Ractor.make_shareable(tc).very_unique + end; + end + def test_too_complex_obj_ivar_ractor_share assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; @@ -632,10 +730,10 @@ class TestShapes < Test::Unit::TestCase r = Ractor.new do o = Object.new o.instance_variable_set(:@a, "hello") - Ractor.yield(o) + o end - o = r.take + o = r.value assert_equal "hello", o.instance_variable_get(:@a) end; end @@ -650,10 +748,10 @@ class TestShapes < Test::Unit::TestCase r = Ractor.new do o = [] o.instance_variable_set(:@a, "hello") - Ractor.yield(o) + o end - o = r.take + o = r.value assert_equal "hello", o.instance_variable_get(:@a) end; end @@ -721,9 +819,42 @@ class TestShapes < Test::Unit::TestCase assert_equal 3, tc.a3_m # make sure IV is initialized assert tc.instance_variable_defined?(:@a3) tc.remove_instance_variable(:@a3) + refute tc.instance_variable_defined?(:@a3) + assert_nil tc.a3 + end + + def test_delete_iv_after_complex_and_object_id + ensure_complex + + tc = TooComplex.new + tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") + assert_predicate RubyVM::Shape.of(tc), :too_complex? + + assert_equal 3, tc.a3_m # make sure IV is initialized + assert tc.instance_variable_defined?(:@a3) + tc.object_id + tc.remove_instance_variable(:@a3) + refute tc.instance_variable_defined?(:@a3) assert_nil tc.a3 end + def test_delete_iv_after_complex_and_freeze + ensure_complex + + tc = TooComplex.new + tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") + assert_predicate RubyVM::Shape.of(tc), :too_complex? + + assert_equal 3, tc.a3_m # make sure IV is initialized + assert tc.instance_variable_defined?(:@a3) + tc.freeze + assert_raise FrozenError do + tc.remove_instance_variable(:@a3) + end + assert tc.instance_variable_defined?(:@a3) + assert_equal 3, tc.a3 + end + def test_delete_undefined_after_complex ensure_complex @@ -786,13 +917,15 @@ class TestShapes < Test::Unit::TestCase def test_remove_instance_variable_capacity_transition assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; - t_object_shape = RubyVM::Shape.find_by_id(RubyVM::Shape::FIRST_T_OBJECT_SHAPE_ID) - assert_equal(RubyVM::Shape::SHAPE_T_OBJECT, t_object_shape.type) - - initial_capacity = t_object_shape.capacity # a does not transition in capacity a = Class.new.new + root_shape = RubyVM::Shape.of(a) + + assert_equal(RubyVM::Shape::SHAPE_ROOT, root_shape.type) + initial_capacity = root_shape.capacity + refute_equal(0, initial_capacity) + initial_capacity.times do |i| a.instance_variable_set(:"@ivar#{i + 1}", i) end @@ -852,13 +985,13 @@ class TestShapes < Test::Unit::TestCase def test_iv_index example = RemoveAndAdd.new initial_shape = RubyVM::Shape.of(example) - assert_equal 0, initial_shape.next_iv_index + assert_equal 0, initial_shape.next_field_index example.add_foo # makes a transition add_foo_shape = RubyVM::Shape.of(example) assert_equal([:@foo], example.instance_variables) - assert_equal(initial_shape.id, add_foo_shape.parent.id) - assert_equal(1, add_foo_shape.next_iv_index) + assert_equal(initial_shape.raw_id, add_foo_shape.parent.raw_id) + assert_equal(1, add_foo_shape.next_field_index) example.remove_foo # makes a transition remove_foo_shape = RubyVM::Shape.of(example) @@ -868,8 +1001,8 @@ 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.id, bar_shape.parent_id) - assert_equal(1, bar_shape.next_iv_index) + assert_equal(initial_shape.raw_id, bar_shape.parent_id) + assert_equal(1, bar_shape.next_field_index) end def test_remove_then_add_again @@ -888,7 +1021,7 @@ class TestShapes < Test::Unit::TestCase def test_new_obj_has_t_object_shape obj = TestObject.new shape = RubyVM::Shape.of(obj) - assert_equal RubyVM::Shape::SHAPE_T_OBJECT, shape.type + assert_equal RubyVM::Shape::SHAPE_ROOT, shape.type assert_nil shape.parent end @@ -900,16 +1033,29 @@ class TestShapes < Test::Unit::TestCase assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of([])) end - def test_true_has_special_const_shape_id - assert_equal(RubyVM::Shape::SPECIAL_CONST_SHAPE_ID, RubyVM::Shape.of(true).id) - end - - def test_nil_has_special_const_shape_id - assert_equal(RubyVM::Shape::SPECIAL_CONST_SHAPE_ID, RubyVM::Shape.of(nil).id) + def test_raise_on_special_consts + assert_raise ArgumentError do + RubyVM::Shape.of(true) + end + assert_raise ArgumentError do + RubyVM::Shape.of(false) + end + assert_raise ArgumentError do + RubyVM::Shape.of(nil) + end + assert_raise ArgumentError do + RubyVM::Shape.of(0) + end + # 32-bit platforms don't have flonums or static symbols as special + # constants + # TODO(max): Add ArgumentError tests for symbol and flonum, skipping if + # RUBY_PLATFORM =~ /i686/ end - def test_root_shape_transition_to_special_const_on_frozen - assert_equal(RubyVM::Shape::SPECIAL_CONST_SHAPE_ID, RubyVM::Shape.of([].freeze).id) + 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) end def test_basic_shape_transition @@ -920,7 +1066,7 @@ class TestShapes < Test::Unit::TestCase assert_equal RubyVM::Shape::SHAPE_IVAR, shape.type shape = shape.parent - assert_equal RubyVM::Shape::SHAPE_T_OBJECT, shape.type + assert_equal RubyVM::Shape::SHAPE_ROOT, shape.type assert_nil shape.parent assert_equal(1, obj.instance_variable_get(:@a)) @@ -955,11 +1101,12 @@ class TestShapes < Test::Unit::TestCase def test_freezing_and_duplicating_object obj = Object.new.freeze + assert_predicate(RubyVM::Shape.of(obj), :shape_frozen?) + + # dup'd objects shouldn't be frozen obj2 = obj.dup refute_predicate(obj2, :frozen?) - # dup'd objects shouldn't be frozen, and the shape should be the - # parent shape of the copied object - assert_equal(RubyVM::Shape.of(obj).parent.id, RubyVM::Shape.of(obj2).id) + refute_predicate(RubyVM::Shape.of(obj2), :shape_frozen?) end def test_freezing_and_duplicating_object_with_ivars @@ -976,6 +1123,7 @@ class TestShapes < Test::Unit::TestCase str.freeze str2 = str.dup refute_predicate(str2, :frozen?) + refute_equal(RubyVM::Shape.of(str).id, RubyVM::Shape.of(str2).id) assert_equal(str2.instance_variable_get(:@a), 1) end @@ -992,8 +1140,7 @@ class TestShapes < Test::Unit::TestCase obj2 = obj.clone(freeze: true) assert_predicate(obj2, :frozen?) refute_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) - assert_equal(RubyVM::Shape::SHAPE_FROZEN, RubyVM::Shape.of(obj2).type) - assert_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2).parent) + assert_predicate(RubyVM::Shape.of(obj2), :shape_frozen?) end def test_freezing_and_cloning_object_with_ivars @@ -1036,4 +1183,30 @@ class TestShapes < Test::Unit::TestCase tc.send("a#{_1}_m") end end + + def assert_too_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), :too_complex? + (RubyVM::Shape::SHAPE_MAX_VARIATIONS * 2).times do |i| + obj.remove_instance_variable("@ivar#{i}") + end + assert_predicate RubyVM::Shape.of(obj), :too_complex? + end + + def test_object_too_complex_during_delete + assert_too_complex_during_delete(Class.new.new) + end + + def test_class_too_complex_during_delete + assert_too_complex_during_delete(Module.new) + end + + def test_generic_too_complex_during_delete + assert_too_complex_during_delete(Class.new(Array).new) + end end if defined?(RubyVM::Shape) diff --git a/test/ruby/test_signal.rb b/test/ruby/test_signal.rb index a2bdf02b88..091a66d5da 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})" 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 1011967fe9..2458d38ef4 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -872,6 +872,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 } @@ -1869,6 +1873,13 @@ CODE result = []; S("aaa,bbb,ccc,ddd").split(/,/) {|s| result << s.gsub(/./, "A")} assert_equal(["AAA"]*4, result) + + s = S("abc ") * 20 + assert_raise(RuntimeError) { + 10.times do + s.split {s.prepend("xxx" * 100)} + end + } ensure EnvUtil.suppress_warning {$; = fs} end @@ -1876,9 +1887,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' @@ -2027,6 +2053,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>'))) @@ -2462,33 +2599,7 @@ CODE assert_equal([0xa9, 0x42, 0x2260], S("\xc2\xa9B\xe2\x89\xa0").unpack(S("U*"))) -=begin - skipping "Not tested: - D,d & double-precision float, native format\\ - E & double-precision float, little-endian byte order\\ - e & single-precision float, little-endian byte order\\ - F,f & single-precision float, native format\\ - G & double-precision float, network (big-endian) byte order\\ - g & single-precision float, network (big-endian) byte order\\ - I & unsigned integer\\ - i & integer\\ - L & unsigned long\\ - l & long\\ - - m & string encoded in base64 (uuencoded)\\ - N & long, network (big-endian) byte order\\ - n & short, network (big-endian) byte-order\\ - P & pointer to a structure (fixed-length string)\\ - p & pointer to a null-terminated string\\ - S & unsigned short\\ - s & short\\ - V & long, little-endian byte order\\ - v & short, little-endian byte order\\ - X & back up a byte\\ - x & null byte\\ - Z & ASCII string (null padded, count is width)\\ -" -=end + # more comprehensive tests are in test_pack.rb end def test_upcase @@ -2780,14 +2891,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 @@ -2838,27 +2956,45 @@ CODE assert_equal("\u3042", ("\u3042" * 100)[-1]) end -=begin def test_compare_different_encoding_string s1 = S("\xff".force_encoding("UTF-8")) s2 = S("\xff".force_encoding("ISO-2022-JP")) assert_equal([-1, 1], [s1 <=> s2, s2 <=> s1].sort) + + s3 = S("あ".force_encoding("UTF-16LE")) + s4 = S("a".force_encoding("IBM437")) + assert_equal([-1, 1], [s3 <=> s4, s4 <=> s3].sort) end -=end def test_casecmp assert_equal(0, S("FoO").casecmp("fOO")) assert_equal(1, S("FoO").casecmp("BaR")) + assert_equal(-1, S("foo").casecmp("FOOBAR")) assert_equal(-1, S("baR").casecmp("FoO")) assert_equal(1, S("\u3042B").casecmp("\u3042a")) assert_equal(-1, S("foo").casecmp("foo\0")) + assert_equal(1, S("FOOBAR").casecmp("foo")) + assert_equal(0, S("foo\0bar").casecmp("FOO\0BAR")) assert_nil(S("foo").casecmp(:foo)) assert_nil(S("foo").casecmp(Object.new)) + assert_nil(S("foo").casecmp(0)) + assert_nil(S("foo").casecmp(5.00)) + o = Object.new def o.to_str; "fOO"; end assert_equal(0, S("FoO").casecmp(o)) + + assert_equal(0, S("#" * 128 + "A" * 256 + "b").casecmp("#" * 128 + "a" * 256 + "B")) + assert_equal(0, S("a" * 256 + "B").casecmp("A" * 256 + "b")) + + assert_equal(-1, S("@").casecmp("`")) + assert_equal(0, S("hello\u00E9X").casecmp("HELLO\u00E9x")) + + s1 = S("\xff".force_encoding("UTF-8")) + s2 = S("\xff".force_encoding("ISO-2022-JP")) + assert_nil(s1.casecmp(s2)) end def test_casecmp? @@ -2871,9 +3007,16 @@ CODE assert_nil(S("foo").casecmp?(:foo)) assert_nil(S("foo").casecmp?(Object.new)) + assert_nil(S("foo").casecmp(0)) + assert_nil(S("foo").casecmp(5.00)) + o = Object.new def o.to_str; "fOO"; end assert_equal(true, S("FoO").casecmp?(o)) + + s1 = S("\xff".force_encoding("UTF-8")) + s2 = S("\xff".force_encoding("ISO-2022-JP")) + assert_nil(s1.casecmp?(s2)) end def test_upcase2 @@ -2946,7 +3089,6 @@ CODE s5 = S("\u0000\u3042") assert_equal("\u3042", s5.lstrip!) assert_equal("\u3042", s5) - end def test_delete_prefix_type_error @@ -3246,18 +3388,12 @@ CODE assert_equal('"\\u3042\\u3044\\u3046"', S("\u3042\u3044\u3046".encode(e)).inspect) assert_equal('"ab\\"c"', S("ab\"c".encode(e)).inspect, bug4081) end - begin - verbose, $VERBOSE = $VERBOSE, nil - ext = Encoding.default_external - Encoding.default_external = "us-ascii" - $VERBOSE = verbose + + EnvUtil.with_default_external(Encoding::US_ASCII) do i = S("abc\"\\".force_encoding("utf-8")).inspect - ensure - $VERBOSE = nil - Encoding.default_external = ext - $VERBOSE = verbose + + assert_equal('"abc\\"\\\\"', i, bug4081) end - assert_equal('"abc\\"\\\\"', i, bug4081) end def test_dummy_inspect @@ -3314,6 +3450,11 @@ CODE assert_equal(u("\x82")+("\u3042"*9), S("\u3042"*10).byteslice(2, 28)) + assert_equal("\xE3", S("こんにちは").byteslice(0)) + assert_equal("こんにちは", S("こんにちは").byteslice(0, 15)) + assert_equal("こ", S("こんにちは").byteslice(0, 3)) + assert_equal("は", S("こんにちは").byteslice(12, 15)) + bug7954 = '[ruby-dev:47108]' assert_equal(false, S("\u3042").byteslice(0, 2).valid_encoding?, bug7954) assert_equal(false, ("\u3042"*10).byteslice(0, 20).valid_encoding?, bug7954) @@ -3437,6 +3578,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) @@ -3743,6 +3895,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) @@ -3789,6 +4031,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 3d727adf04..01e5cc68f6 100644 --- a/test/ruby/test_struct.rb +++ b/test/ruby/test_struct.rb @@ -535,6 +535,8 @@ module TestStruct end def test_named_structs_are_not_rooted + 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) code = proc do @@ -542,12 +544,18 @@ module TestStruct Struct.send(:remove_const, :A) end - 1_000.times(&code) + 10_000.times(&code) PREP 50_000.times(&code) CODE end + def test_frozen_subclass + test = Class.new(@Struct.new(:a)).freeze.new(a: 0) + assert_kind_of(@Struct, test) + assert_equal([:a], test.members) + end + class TopStruct < Test::Unit::TestCase include TestStruct diff --git a/test/ruby/test_super.rb b/test/ruby/test_super.rb index 8e973b0f7f..25bad2242a 100644 --- a/test/ruby/test_super.rb +++ b/test/ruby/test_super.rb @@ -759,4 +759,19 @@ 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_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_syntax.rb b/test/ruby/test_syntax.rb index 62f1d99bdc..b355128a73 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -1259,6 +1259,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/) @@ -1794,15 +1840,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 @@ -1946,6 +1989,28 @@ eom end assert_valid_syntax('proc {def foo(_);end;it}') assert_syntax_error('p { [it **2] }', /unexpected \*\*/) + assert_equal(1, eval('1.then { raise rescue it }')) + assert_equal(2, eval('1.then { 2.then { raise rescue it } }')) + assert_equal(3, eval('3.then { begin; raise; rescue; it; end }')) + assert_equal(4, eval('4.tap { begin; raise ; rescue; raise rescue it; end; }')) + assert_equal(5, eval('a = 0; 5.then { begin; nil; ensure; a = it; end }; a')) + assert_equal(6, eval('a = 0; 6.then { begin; nil; rescue; ensure; a = it; end }; a')) + assert_equal(7, eval('a = 0; 7.then { begin; raise; ensure; a = it; end } rescue a')) + assert_equal(8, eval('a = 0; 8.then { begin; raise; rescue; ensure; a = it; end }; a')) + 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 def test_value_expr_in_condition @@ -2018,10 +2083,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) @@ -2050,7 +2116,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}) } @@ -2230,13 +2296,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..2a61fc3450 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -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,65 @@ q.pop frame_for_deadlock_test_2 { t.join } INPUT end + + # [Bug #21342] + def test_unlock_locked_mutex_with_collected_fiber + bug21127 = '[ruby-core:120930] [Bug #21127]' + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + 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 end diff --git a/test/ruby/test_thread_cv.rb b/test/ruby/test_thread_cv.rb index eb88b9606c..81201f134f 100644 --- a/test/ruby/test_thread_cv.rb +++ b/test/ruby/test_thread_cv.rb @@ -76,7 +76,7 @@ class TestThreadConditionVariable < Test::Unit::TestCase 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 545bf98888..9a41be8b1a 100644 --- a/test/ruby/test_thread_queue.rb +++ b/test/ruby/test_thread_queue.rb @@ -235,8 +235,14 @@ class TestThreadQueue < Test::Unit::TestCase end _eom rescue Timeout::Error + # record load average: + uptime = `uptime` rescue nil + if uptime && /(load average: [\d.]+),/ =~ uptime + la = " (#{$1})" + end + count = File.read("#{d}/test_thr_kill_count").to_i - flunk "only #{count}/#{total_count} done in #{timeout} seconds." + flunk "only #{count}/#{total_count} done in #{timeout} seconds.#{la}" end } end @@ -373,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 @@ -427,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 @@ -561,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_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 63d37f4ba4..99b5ee8d43 100644 --- a/test/ruby/test_transcode.rb +++ b/test/ruby/test_transcode.rb @@ -2320,6 +2320,93 @@ class TestTranscode < Test::Unit::TestCase assert_equal("A\nB\nC", s.encode(usascii, newline: :lf)) end + def test_ractor_lazy_load_encoding + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) + begin; + rs = [] + autoload_encodings = Encoding.list.select { |e| e.inspect.include?("(autoload)") }.freeze + 7.times do + rs << Ractor.new(autoload_encodings) do |encodings| + str = "\u0300" + encodings.each do |enc| + str.encode(enc) rescue Encoding::UndefinedConversionError + end + end + end + + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + 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;'}") + begin; + rs = [] + 100.times do + rs << Ractor.new do + "\u0300".encode(Encoding.list.sample) rescue Encoding::UndefinedConversionError + end + end + + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + assert_empty rs + end; + end + + def test_ractor_asciicompat_encoding_exists + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + rs = [] + 7.times do + rs << Ractor.new do + string = "ISO-2022-JP" + encoding = Encoding.find(string) + 20_000.times do + Encoding::Converter.asciicompat_encoding(string) + Encoding::Converter.asciicompat_encoding(encoding) + end + end + end + + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + assert_empty rs + end; + end + + def test_ractor_asciicompat_encoding_doesnt_exist + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) + begin; + rs = [] + NO_EXIST = "I".freeze + 7.times do + rs << Ractor.new do + 50.times do + if (val = Encoding::Converter.asciicompat_encoding(NO_EXIST)) + raise "Got #{val}, expected nil" + end + end + end + end + + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + assert_empty rs + end; + end + private def assert_conversion_both_ways_utf8(utf8, raw, encoding) diff --git a/test/ruby/test_variable.rb b/test/ruby/test_variable.rb index 49fec2d40e..13b8a7905f 100644 --- a/test/ruby/test_variable.rb +++ b/test/ruby/test_variable.rb @@ -388,6 +388,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 @@ -407,6 +462,21 @@ class TestVariable < Test::Unit::TestCase } end + def test_exivar_resize_with_compaction_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + objs = 10_000.times.map do + ExIvar.new + end + EnvUtil.under_gc_compact_stress do + 10.times do + x = ExIvar.new + x.instance_variable_set(:@resize, 1) + x + end + end + objs or flunk + end + def test_local_variables_with_kwarg bug11674 = '[ruby-core:71437] [Bug #11674]' v = with_kwargs_11(v1:1,v2:2,v3:3,v4:4,v5:5,v6:6,v7:7,v8:8,v9:9,v10:10,v11:11) @@ -426,12 +496,55 @@ class TestVariable < Test::Unit::TestCase end def test_local_variables_encoding - α = 1 + α = 1 or flunk b = binding b.eval("".encode("us-ascii")) assert_equal(%i[α b], b.local_variables) end + def test_genivar_cache + bug21547 = '[Bug #21547]' + klass = Class.new(Array) + instance = klass.new + instance.instance_variable_set(:@a1, 1) + instance.instance_variable_set(:@a2, 2) + Fiber.new do + instance.instance_variable_set(:@a3, 3) + instance.instance_variable_set(:@a4, 4) + end.resume + 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 def with_kwargs_11(v1:, v2:, v3:, v4:, v5:, v6:, v7:, v8:, v9:, v10:, v11:) local_variables diff --git a/test/ruby/test_vm_dump.rb b/test/ruby/test_vm_dump.rb index 709fd5eadf..d183e03391 100644 --- a/test/ruby/test_vm_dump.rb +++ b/test/ruby/test_vm_dump.rb @@ -5,8 +5,7 @@ return unless /darwin/ =~ RUBY_PLATFORM class TestVMDump < Test::Unit::TestCase def assert_darwin_vm_dump_works(args, timeout=nil) - pend "macOS 15 is not working with this assertion" if macos?(15) - + args.unshift({"RUBY_ON_BUG" => nil, "RUBY_CRASH_REPORT" => nil}) assert_in_out_err(args, "", [], /^\[IMPORTANT\]/, timeout: timeout || 300) end @@ -15,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..4f5823ecf4 100644 --- a/test/ruby/test_weakmap.rb +++ b/test/ruby/test_weakmap.rb @@ -203,7 +203,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 +224,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 +265,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 7c0524354b..d6b9b75648 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) @@ -166,6 +166,11 @@ class TestYJIT < Test::Unit::TestCase end end + if JITSupport.zjit_supported? + def test_yjit_enable_with_zjit_enabled + assert_in_out_err(['--zjit'], 'puts RubyVM::YJIT.enable', ['false'], ['Only one JIT can be enabled at the same time.']) + end + end def test_yjit_stats_and_v_no_error _stdout, stderr, _status = invoke_ruby(%w(-v --yjit-stats), '', true, true) @@ -652,6 +657,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 @@ -1529,14 +1554,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 @@ -1775,6 +1792,30 @@ 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 + private def code_gc_helpers diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 5336c6cc47..ad2df806d5 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -9,6 +9,109 @@ require_relative '../lib/jit_support' return unless JITSupport.zjit_supported? class TestZJIT < Test::Unit::TestCase + def test_enabled + assert_runs 'false', <<~RUBY, zjit: false + RubyVM::ZJIT.enabled? + RUBY + assert_runs 'true', <<~RUBY, zjit: true + RubyVM::ZJIT.enabled? + RUBY + end + + def test_stats_enabled + assert_runs 'false', <<~RUBY, stats: false + RubyVM::ZJIT.stats_enabled? + RUBY + assert_runs 'true', <<~RUBY, stats: true + RubyVM::ZJIT.stats_enabled? + RUBY + end + + def test_stats_quiet + # Test that --zjit-stats-quiet collects stats but doesn't print them + script = <<~RUBY + def test = 42 + test + test + puts RubyVM::ZJIT.stats_enabled? + RUBY + + stats_header = "***ZJIT: Printing ZJIT statistics on exit***" + + # 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) + + # 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) + + # 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 + + 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_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_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" + + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY + end + + def test_zjit_disable + assert_separately(["--zjit", "--zjit-disable"], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" + + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY + 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? + + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? + RUBY + end + def test_call_itself assert_compiles '42', <<~RUBY, call_threshold: 2 def test = 42.itself @@ -31,6 +134,20 @@ class TestZJIT < Test::Unit::TestCase } end + def test_putstring + assert_compiles '""', %q{ + def test = "#{""}" + test + }, insns: [:putstring] + end + + def test_putchilldedstring + assert_compiles '""', %q{ + def test = "" + test + }, insns: [:putchilledstring] + end + def test_leave_param assert_compiles '5', %q{ def test(n) = n @@ -38,6 +155,194 @@ class TestZJIT < Test::Unit::TestCase } end + def test_getglobal_with_warning + assert_compiles('"rescued"', %q{ + Warning[:deprecated] = true + + module Warning + def warn(message) + raise + end + end + + def test + $= + rescue + "rescued" + end + + $VERBOSE = true + test + }, insns: [:getglobal]) + end + + def test_setglobal + assert_compiles '1', %q{ + def test + $a = 1 + $a + end + + test + }, insns: [:setglobal] + end + + def test_string_intern + assert_compiles ':foo123', %q{ + def test + :"foo#{123}" + end + + test + }, insns: [:intern] + end + + def test_duphash + assert_compiles '{a: 1}', %q{ + def test + {a: 1} + end + + test + }, insns: [:duphash] + end + + def test_pushtoarray + assert_compiles '[1, 2, 3]', %q{ + def test + [*[], 1, 2, 3] + end + test + }, insns: [:pushtoarray] + end + + def test_splatarray_new_array + assert_compiles '[1, 2, 3]', %q{ + def test a + [*a, 3] + end + test [1, 2] + }, insns: [:splatarray] + end + + def test_splatarray_existing_array + assert_compiles '[1, 2, 3]', %q{ + def foo v + [1, 2, v] + end + def test a + foo(*a) + end + test [3] + }, insns: [:splatarray] + end + + def test_concattoarray + assert_compiles '[1, 2, 3]', %q{ + def test(*a) + [1, 2, *a] + end + test 3 + }, insns: [:concattoarray] + end + + def test_definedivar + assert_compiles '[nil, "instance-variable", nil]', %q{ + def test + v0 = defined?(@a) + @a = nil + v1 = defined?(@a) + remove_instance_variable :@a + v2 = defined?(@a) + [v0, v1, v2] + end + test + }, insns: [:definedivar] + end + + 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 + + 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_toplevel_local_after_eval + # 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 + eval('b = 3') + c = 4 + print [a, b, c] + }) + assert_success(out, err, status) + assert_equal "[1, 3, 4]", out + end + + def test_getlocal_after_eval + assert_compiles '2', %q{ + def test + a = 1 + eval('a = 2') + a + end + test + } + end + + def test_getlocal_after_instance_eval + assert_compiles '2', %q{ + def test + a = 1 + instance_eval('a = 2') + a + end + test + } + end + + def test_getlocal_after_module_eval + assert_compiles '2', %q{ + def test + a = 1 + Kernel.module_eval('a = 2') + a + end + test + } + end + + def test_getlocal_after_class_eval + assert_compiles '2', %q{ + def test + a = 1 + Kernel.class_eval('a = 2') + a + end + test + } + end + def test_setlocal assert_compiles '3', %q{ def test(n) @@ -48,6 +353,247 @@ class TestZJIT < Test::Unit::TestCase } end + def test_return_nonparam_local + # Use dead code (if false) to create a local without initialization instructions. + assert_compiles 'nil', %q{ + def foo(a) + if false + x = nil + end + x + end + def test = foo(1) + test + test + }, call_threshold: 2 + end + + def test_nonparam_local_nil_in_jit_call + # Non-parameter locals must be initialized to nil in JIT-to-JIT calls. + # Use dead code (if false) to create locals without initialization instructions. + # Then eval a string that accesses the uninitialized locals. + assert_compiles '["x", "x", "x", "x"]', %q{ + def f(a) + a ||= 1 + if false; b = 1; end + eval("-> { p 'x#{b}' }") + end + + 4.times.map { f(1).call } + }, call_threshold: 2 + end + + def test_kwargs_with_exit_and_local_invalidation + assert_compiles ':ok', %q{ + def a(b:, c:) + if c == :b + return -> {} + end + Class # invalidate locals + + raise "c is :b!" if c == :b + end + + def test + # note opposite order of kwargs + a(c: :c, b: :b) + end + + 4.times { test } + :ok + }, call_threshold: 2 + end + + def test_kwargs_with_max_direct_send_arg_count + # Ensure that we only reorder the args when we _can_ use direct send (< 6 args). + assert_compiles '[[1, 2, 3, 4, 5, 6, 7, 8]]', %q{ + def kwargs(five, six, a:, b:, c:, d:, e:, f:) + [a, b, c, d, five, six, e, f] + end + + 5.times.flat_map do + [ + kwargs(5, 6, d: 4, c: 3, a: 1, b: 2, e: 7, f: 8), + kwargs(5, 6, d: 4, c: 3, b: 2, a: 1, e: 7, f: 8) + ] + end.uniq + }, call_threshold: 2 + end + + def test_setlocal_on_eval + assert_compiles '1', %q{ + @b = binding + eval('a = 1', @b) + eval('a', @b) + } + end + + def test_optional_arguments + assert_compiles '[[1, 2, 3], [10, 20, 3], [100, 200, 300]]', %q{ + def test(a, b = 2, c = 3) + [a, b, c] + end + [test(1), test(10, 20), test(100, 200, 300)] + } + end + + def test_optional_arguments_setlocal + assert_compiles '[[2, 2], [1, nil]]', %q{ + def test(a = (b = 2)) + [a, b] + end + [test, test(1)] + } + end + + def test_optional_arguments_cyclic + assert_compiles '[nil, 1]', %q{ + test = proc { |a=a| a } + [test.call, test.call(1)] + } + end + + def test_optional_arguments_side_exit + # This leads to FailedOptionalArguments, so not using assert_compiles + assert_runs '[:foo, nil, 1]', %q{ + def test(a = (def foo = nil)) = a + [test, (undef :foo), test(1)] + } + end + + def test_getblockparamproxy + assert_compiles '1', %q{ + def test(&block) + 0.then(&block) + end + test { 1 } + }, insns: [:getblockparamproxy] + end + + def test_optimized_method_call_proc_call + assert_compiles '2', %q{ + p = proc { |x| x * 2 } + def test(p) + p.call(1) + end + test(p) + test(p) + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_optimized_method_call_proc_aref + assert_compiles '4', %q{ + p = proc { |x| x * 2 } + def test(p) + p[2] + end + test(p) + test(p) + }, call_threshold: 2, insns: [:opt_aref] + end + + def test_optimized_method_call_proc_yield + assert_compiles '6', %q{ + p = proc { |x| x * 2 } + def test(p) + p.yield(3) + end + test(p) + test(p) + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_optimized_method_call_proc_kw_splat + assert_compiles '3', %q{ + p = proc { |**kw| kw[:a] + kw[:b] } + def test(p, h) + p.call(**h) + end + h = { a: 1, b: 2 } + test(p, h) + test(p, h) + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_optimized_method_call_proc_call_splat + assert_compiles '43', %q{ + p = proc { |x| x + 1 } + def test(p) + ary = [42] + p.call(*ary) + end + test(p) + test(p) + }, call_threshold: 2 + end + + def test_optimized_method_call_proc_call_kwarg + assert_compiles '1', %q{ + p = proc { |a:| a } + def test(p) + p.call(a: 1) + end + test(p) + test(p) + }, call_threshold: 2 + end + + def test_call_a_forwardable_method + assert_runs '[]', %q{ + def test_root = forwardable + def forwardable(...) = Array.[](...) + test_root + test_root + }, call_threshold: 2 + 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_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 + + test + test + test + }, call_threshold: 3, insns: [:getlocal, :setlocal, :getlocal_WC_0, :setlocal_WC_1] + end + + def test_send_with_local_written_by_blockiseq + assert_compiles '[1, 2]', %q{ + def test + l1 = nil + l2 = nil + tap do |_| + l1 = 1 + tap do |_| + l2 = 2 + end + end + + [l1, l2] + end + + test + test + }, call_threshold: 2 + end + def test_send_without_block assert_compiles '[1, 2, 3]', %q{ def foo = 1 @@ -62,6 +608,802 @@ class TestZJIT < Test::Unit::TestCase } 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 + foo(1, 2, 3, 4, 5, 6) + end + + test # profile send + test + }, call_threshold: 2 + 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 + end + + def test_send_exit_with_uninitialized_locals + assert_runs 'nil', %q{ + def entry(init) + function_stub_exit(init) + end + + def function_stub_exit(init) + uninitialized_local = 1 if init + uninitialized_local + end + + entry(true) # profile and set 1 to the local slot + entry(false) + }, call_threshold: 2, allowed_iseqs: 'entry@-e:2' + end + + def test_send_optional_arguments + assert_compiles '[[1, 2], [3, 4]]', %q{ + def test(a, b = 2) = [a, b] + def entry = [test(1), test(3, 4)] + entry + entry + }, call_threshold: 2 + end + + def test_send_nil_block_arg + assert_compiles 'false', %q{ + def test = block_given? + def entry = test(&nil) + test + } + end + + def test_send_symbol_block_arg + assert_compiles '["1", "2"]', %q{ + def test = [1, 2].map(&:to_s) + test + } + end + + def test_send_variadic_with_block + assert_compiles '[[1, "a"], [2, "b"], [3, "c"]]', %q{ + A = [1, 2, 3] + B = ["a", "b", "c"] + + def test + result = [] + A.zip(B) { |x, y| result << [x, y] } + result + end + + test; test + }, call_threshold: 2 + end + + def test_send_splat + assert_runs '[1, 2]', %q{ + def test(a, b) = [a, b] + def entry(arr) = test(*arr) + entry([1, 2]) + } + end + + def test_send_kwarg + assert_runs '[1, 2]', %q{ + def test(a:, b:) = [a, b] + def entry = test(b: 2, a: 1) # change order + entry + entry + }, call_threshold: 2 + end + + def test_send_kwarg_optional + assert_compiles '[1, 2]', %q{ + def test(a: 1, b: 2) = [a, b] + def entry = test + entry + entry + }, call_threshold: 2 + end + + def test_send_kwarg_optional_too_many + assert_compiles '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]', %q{ + def test(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10) = [a, b, c, d, e, f, g, h, i, j] + def entry = test + entry + entry + }, call_threshold: 2 + end + + def test_send_kwarg_required_and_optional + assert_compiles '[3, 2]', %q{ + def test(a:, b: 2) = [a, b] + def entry = test(a: 3) + entry + entry + }, call_threshold: 2 + end + + def test_send_kwarg_to_hash + assert_compiles '{a: 3}', %q{ + def test(hash) = hash + def entry = test(a: 3) + entry + entry + }, call_threshold: 2 + end + + def test_send_kwarg_to_ccall + assert_compiles '["a", "b", "c"]', %q{ + def test(s) = s.each_line(chomp: true).to_a + def entry = test(%(a\nb\nc)) + entry + entry + }, call_threshold: 2 + end + + def test_send_kwarg_and_block_to_ccall + assert_compiles '["a", "b", "c"]', %q{ + def test(s) + a = [] + s.each_line(chomp: true) { |l| a << l } + a + end + def entry = test(%(a\nb\nc)) + entry + entry + }, call_threshold: 2 + end + + def test_send_kwarg_with_too_many_args_to_c_call + assert_compiles '"a b c d {kwargs: :e}"', %q{ + def test(a:, b:, c:, d:, e:) = sprintf("%s %s %s %s %s", a, b, c, d, kwargs: e) + def entry = test(e: :e, d: :d, c: :c, a: :a, b: :b) + entry + entry + }, call_threshold: 2 + end + + def test_send_kwsplat + assert_compiles '3', %q{ + def test(a:) = a + def entry = test(**{a: 3}) + entry + entry + }, call_threshold: 2 + end + + def test_send_kwrest + assert_compiles '{a: 3}', %q{ + def test(**kwargs) = kwargs + def entry = test(a: 3) + entry + entry + }, call_threshold: 2 + end + + def test_send_req_kwreq + assert_compiles '[1, 3]', %q{ + def test(a, c:) = [a, c] + def entry = test(1, c: 3) + entry + entry + }, call_threshold: 2 + end + + def test_send_req_opt_kwreq + assert_compiles '[[1, 2, 3], [-1, -2, -3]]', %q{ + def test(a, b = 2, c:) = [a, b, c] + def entry = [test(1, c: 3), test(-1, -2, c: -3)] # specify all, change kw order + entry + entry + }, call_threshold: 2 + end + + def test_send_req_opt_kwreq_kwopt + assert_compiles '[[1, 2, 3, 4], [-1, -2, -3, -4]]', %q{ + def test(a, b = 2, c:, d: 4) = [a, b, c, d] + def entry = [test(1, c: 3), test(-1, -2, d: -4, c: -3)] # specify all, change kw order + entry + entry + }, call_threshold: 2 + end + + def test_send_unexpected_keyword + assert_compiles ':error', %q{ + def test(a: 1) = a*2 + def entry + test(z: 2) + rescue ArgumentError + :error + end + + entry + entry + }, call_threshold: 2 + end + + def test_send_all_arg_types + assert_compiles '[:req, :opt, :post, :kwr, :kwo, true]', %q{ + def test(a, b = :opt, c, d:, e: :kwo) = [a, b, c, d, e, block_given?] + def entry = test(:req, :post, d: :kwr) {} + entry + entry + }, call_threshold: 2 + end + + def test_send_ccall_variadic_with_different_receiver_classes + assert_compiles '[true, true]', %q{ + def test(obj) = obj.start_with?("a") + [test("abc"), test(:abc)] + }, call_threshold: 2 + end + + def test_forwardable_iseq + assert_compiles '1', %q{ + def test(...) = 1 + test + } + end + + def test_sendforward + assert_compiles '[1, 2]', %q{ + def callee(a, b) = [a, b] + def test(...) = callee(...) + test(1, 2) + }, insns: [:sendforward] + end + + def test_iseq_with_optional_arguments + assert_compiles '[[1, 2], [3, 4]]', %q{ + def test(a, b = 2) = [a, b] + [test(1), test(3, 4)] + } + end + + def test_invokesuper + assert_compiles '[6, 60]', %q{ + class Foo + def foo(a) = a + 1 + def bar(a) = a + 10 + end + + class Bar < Foo + def foo(a) = super(a) + 2 + def bar(a) = super + 20 + end + + bar = Bar.new + [bar.foo(3), bar.bar(30)] + } + end + + def test_invokesuper_with_local_written_by_blockiseq + # Using `assert_runs` because we don't compile invokeblock yet + assert_runs '3', %q{ + class Foo + def test + yield + end + end + + class Bar < Foo + def test + a = 1 + super do + a += 2 + end + a + end + end + + Bar.new.test + } + end + + def test_invokesuper_to_iseq + assert_compiles '["B", "A"]', %q{ + class A + def foo + "A" + end + end + + class B < A + def foo + ["B", super] + end + end + + def test + B.new.foo + end + + test # profile invokesuper + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_with_args + assert_compiles '["B", 11]', %q{ + class A + def foo(x) + x * 2 + end + end + + class B < A + def foo(x) + ["B", super(x) + 1] + end + end + + def test + B.new.foo(5) + end + + test # profile invokesuper + test # compile + run compiled code + }, call_threshold: 2 + end + + # Test super with explicit args when callee has rest parameter. + # This should fall back to dynamic dispatch since we can't handle rest params yet. + def test_invokesuper_with_args_to_rest_param + assert_compiles '["B", "a", ["b", "c"]]', %q{ + class A + def foo(x, *rest) + [x, rest] + end + end + + class B < A + def foo(x, y, z) + ["B", *super(x, y, z)] + end + end + + def test + B.new.foo("a", "b", "c") + end + + test # profile invokesuper + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_with_block + assert_compiles '["B", "from_block"]', %q{ + class A + def foo + block_given? ? yield : "no_block" + end + end + + class B < A + def foo + ["B", super { "from_block" }] + end + end + + def test + B.new.foo + end + + test # profile invokesuper + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_to_cfunc + assert_compiles '["MyArray", 3]', %q{ + class MyArray < Array + def length + ["MyArray", super] + end + end + + def test + MyArray.new([1, 2, 3]).length + end + + test # profile invokesuper + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_multilevel + assert_compiles '["C", ["B", "A"]]', %q{ + class A + def foo + "A" + end + end + + class B < A + def foo + ["B", super] + end + end + + class C < B + def foo + ["C", super] + end + end + + def test + C.new.foo + end + + test # profile invokesuper + test # compile + run compiled code + }, call_threshold: 2 + end + + # Test implicit block forwarding - super without explicit block should forward caller's block + # Note: We call test twice to ensure ZJIT compiles it before the final call that we check + def test_invokesuper_forwards_block_implicitly + assert_compiles '["B", "forwarded_block"]', %q{ + class A + def foo + block_given? ? yield : "no_block" + end + end + + class B < A + def foo + ["B", super] # should forward the block from caller + end + end + + def test + B.new.foo { "forwarded_block" } + end + + test # profile invokesuper + test # compile + run compiled code + }, call_threshold: 2 + end + + # Test implicit block forwarding with explicit arguments + def test_invokesuper_forwards_block_implicitly_with_args + assert_compiles '["B", ["arg_value", "forwarded"]]', %q{ + class A + def foo(x) + [x, (block_given? ? yield : "no_block")] + end + end + + class B < A + def foo(x) + ["B", super(x)] # explicit args, but block should still be forwarded + end + end + + def test + B.new.foo("arg_value") { "forwarded" } + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + # Test implicit block forwarding when no block is given (should not fail) + def test_invokesuper_forwards_block_implicitly_no_block_given + assert_compiles '["B", "no_block"]', %q{ + class A + def foo + block_given? ? yield : "no_block" + end + end + + class B < A + def foo + ["B", super] # no block given by caller + end + end + + def test + B.new.foo # called without a block + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + # Test implicit block forwarding through multiple inheritance levels + def test_invokesuper_forwards_block_implicitly_multilevel + assert_compiles '["C", ["B", "deep_block"]]', %q{ + class A + def foo + block_given? ? yield : "no_block" + end + end + + class B < A + def foo + ["B", super] # forwards block to A + end + end + + class C < B + def foo + ["C", super] # forwards block to B, which forwards to A + end + end + + def test + C.new.foo { "deep_block" } + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + # Test implicit block forwarding with block parameter syntax + def test_invokesuper_forwards_block_param + assert_compiles '["B", "block_param_forwarded"]', %q{ + class A + def foo + block_given? ? yield : "no_block" + end + end + + class B < A + def foo(&block) + ["B", super] # should forward &block implicitly + end + end + + def test + B.new.foo { "block_param_forwarded" } + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_with_blockarg + assert_compiles '["B", "different block"]', %q{ + class A + def foo + block_given? ? yield : "no block" + end + end + + class B < A + def foo(&blk) + other_block = proc { "different block" } + ["B", super(&other_block)] + end + end + + def test + B.new.foo { "passed block" } + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_with_symbol_to_proc + assert_compiles '["B", [3, 5, 7]]', %q{ + class A + def foo(items, &blk) + items.map(&blk) + end + end + + class B < A + def foo(items) + ["B", super(items, &:succ)] + end + end + + def test + B.new.foo([2, 4, 6]) + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_with_splat + assert_compiles '["B", 6]', %q{ + class A + def foo(a, b, c) + a + b + c + end + end + + class B < A + def foo(*args) + ["B", super(*args)] + end + end + + def test + B.new.foo(1, 2, 3) + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_with_kwargs + assert_compiles '["B", "x=1, y=2"]', %q{ + class A + def foo(x:, y:) + "x=#{x}, y=#{y}" + end + end + + class B < A + def foo(x:, y:) + ["B", super(x: x, y: y)] + end + end + + def test + B.new.foo(x: 1, y: 2) + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + def test_invokesuper_with_kw_splat + assert_compiles '["B", "x=1, y=2"]', %q{ + class A + def foo(x:, y:) + "x=#{x}, y=#{y}" + end + end + + class B < A + def foo(**kwargs) + ["B", super(**kwargs)] + end + end + + def test + B.new.foo(x: 1, y: 2) + end + + test # profile + test # compile + run compiled code + }, call_threshold: 2 + end + + # Test that including a module after compilation correctly changes the super target. + # The included module's method should be called, not the original super target. + def test_invokesuper_with_include + assert_compiles '["B", "M"]', %q{ + class A + def foo + "A" + end + end + + class B < A + def foo + ["B", super] + end + end + + def test + B.new.foo + end + + test # profile invokesuper (super -> A#foo) + test # compile with super -> A#foo + + # Now include a module in B that defines foo - super should go to M#foo instead + module M + def foo + "M" + end + end + B.include(M) + + test # should call M#foo, not A#foo + }, call_threshold: 2 + end + + # Test that prepending a module after compilation correctly changes the super target. + # The prepended module's method should be called, not the original super target. + def test_invokesuper_with_prepend + assert_compiles '["B", "M"]', %q{ + class A + def foo + "A" + end + end + + class B < A + def foo + ["B", super] + end + end + + def test + B.new.foo + end + + test # profile invokesuper (super -> A#foo) + test # compile with super -> A#foo + + # Now prepend a module that defines foo - super should go to M#foo instead + module M + def foo + "M" + end + end + A.prepend(M) + + test # should call M#foo, not A#foo + }, call_threshold: 2 + end + + # Test super with positional and keyword arguments (pattern from chunky_png) + def test_invokesuper_with_keyword_args + assert_compiles '{content: "image data"}', %q{ + class A + def foo(attributes = {}) + @attributes = attributes + end + end + + class B < A + def foo(content = '') + super(content: content) + end + end + + def test + B.new.foo("image data") + end + + test + test + }, call_threshold: 2 + end + + def test_invokebuiltin + # Not using assert_compiles due to register spill + assert_runs '["."]', %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 @@ -86,6 +1428,59 @@ class TestZJIT < Test::Unit::TestCase }, 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{ @@ -104,7 +1499,6 @@ class TestZJIT < Test::Unit::TestCase end def test_opt_mult_overflow - omit 'side exits are not implemented yet' assert_compiles '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{ def test(a, b) a * b @@ -126,7 +1520,15 @@ class TestZJIT < Test::Unit::TestCase def test(a, b) = a == b test(0, 2) # profile opt_eq [test(1, 1), test(0, 1)] - }, call_threshold: 2 + }, 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 @@ -136,7 +1538,7 @@ class TestZJIT < Test::Unit::TestCase def test(a, b) = a != b test(0, 2) # profile opt_neq [test(1, 1), test(0, 1)] - }, call_threshold: 1 + }, insns: [:opt_neq], call_threshold: 1 end def test_opt_neq_fixnum @@ -152,7 +1554,7 @@ class TestZJIT < Test::Unit::TestCase def test(a, b) = a < b test(2, 3) # profile opt_lt [test(0, 1), test(0, 0), test(1, 0)] - }, call_threshold: 2 + }, insns: [:opt_lt], call_threshold: 2 end def test_opt_lt_with_literal_lhs @@ -160,7 +1562,7 @@ class TestZJIT < Test::Unit::TestCase def test(n) = 2 < n test(2) # profile opt_lt [test(1), test(2), test(3)] - }, call_threshold: 2 + }, insns: [:opt_lt], call_threshold: 2 end def test_opt_le @@ -168,7 +1570,7 @@ class TestZJIT < Test::Unit::TestCase def test(a, b) = a <= b test(2, 3) # profile opt_le [test(0, 1), test(0, 0), test(1, 0)] - }, call_threshold: 2 + }, insns: [:opt_le], call_threshold: 2 end def test_opt_gt @@ -176,22 +1578,621 @@ class TestZJIT < Test::Unit::TestCase 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, 2, 4]', %q{ + def test(a, b) = a & b + [ + test(5, 3), + test(0b011, 0b110), + test(-0b011, 0b110) + ] + }, call_threshold: 2, insns: [:opt_and] + end + + def test_fixnum_and_side_exit + assert_compiles '[2, 2, false]', %q{ + def test(a, b) = a & b + [ + test(2, 2), + test(0b011, 0b110), + test(true, false) + ] + }, call_threshold: 2, insns: [:opt_and] + end + + def test_fixnum_or + assert_compiles '[7, 3, -3]', %q{ + def test(a, b) = a | b + [ + test(5, 3), + test(1, 2), + test(1, -4) + ] + }, call_threshold: 2, insns: [:opt_or] + end + + def test_fixnum_or_side_exit + assert_compiles '[3, 2, true]', %q{ + def test(a, b) = a | b + [ + test(1, 2), + test(2, 2), + test(true, false) + ] + }, call_threshold: 2, insns: [:opt_or] + end + + def test_fixnum_xor + assert_compiles '[6, -8, 3]', %q{ + def test(a, b) = a ^ b + [ + test(5, 3), + test(-5, 3), + test(1, 2) + ] + }, call_threshold: 2 + end + + def test_fixnum_xor_side_exit + assert_compiles '[6, 6, true]', %q{ + def test(a, b) = a ^ b + [ + test(5, 3), + test(5, 3), + test(true, false) + ] }, call_threshold: 2 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_fixnum_div + assert_compiles '12', %q{ + C = 48 + def test(n) = C / n + test(4) + test(4) + }, call_threshold: 2, insns: [:opt_div] + end + + def test_fixnum_floor + assert_compiles '0', %q{ + C = 3 + def test(n) = C / n + test(4) + test(4) + }, call_threshold: 2, insns: [:opt_div] + end + + def test_fixnum_div_zero + assert_runs '"divided by 0"', %q{ + def test(n) + n / 0 + rescue ZeroDivisionError => e + e.message + end + + test(0) + test(0) + }, call_threshold: 2, insns: [:opt_div] + 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_opt_new_does_not_push_frame + assert_compiles 'nil', %q{ + class Foo + attr_reader :backtrace + + def initialize + @backtrace = caller + end + end + def test = Foo.new + + foo = test + foo.backtrace.find do |frame| + frame.include?('Class#new') + end + }, insns: [:opt_new] + end + + def test_opt_new_with_redefined + assert_compiles '"foo"', %q{ + class Foo + def self.new = "foo" + + def initialize = raise("unreachable") + end + def test = Foo.new + + test + }, insns: [:opt_new] + end + + def test_opt_new_invalidate_new + assert_compiles '["Foo", "foo"]', %q{ + class Foo; end + def test = Foo.new + test; test + result = [test.class.name] + def Foo.new = "foo" + result << test + result + }, insns: [:opt_new], call_threshold: 2 + end + + 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_opt_new_with_custom_allocator_raises + assert_compiles '[42, 42]', %q{ + require "digest" + class C < Digest::Base; end + def test + begin + Digest::Base.new + rescue NotImplementedError + 42 + end + end + [test, test] + }, insns: [:opt_new], call_threshold: 2 + end + + def test_opt_newarray_send_include_p + assert_compiles '[true, false]', %q{ + def test(x) + [:y, 1, Object.new].include?(x) + end + [test(1), test("n")] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_include_p_redefined + assert_compiles '[:true, :false]', %q{ + class Array + alias_method :old_include?, :include? + def include?(x) + old_include?(x) ? :true : :false + end + end + + def test(x) + [:y, 1, Object.new].include?(x) + end + [test(1), test("n")] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_duparray_send_include_p + assert_compiles '[true, false]', %q{ + def test(x) + [:y, 1].include?(x) + end + [test(1), test("n")] + }, insns: [:opt_duparray_send], call_threshold: 1 + end + + def test_opt_duparray_send_include_p_redefined + assert_compiles '[:true, :false]', %q{ + class Array + alias_method :old_include?, :include? + def include?(x) + old_include?(x) ? :true : :false + end + end + + def test(x) + [:y, 1].include?(x) + end + [test(1), test("n")] + }, insns: [:opt_duparray_send], call_threshold: 1 + end + + def test_opt_newarray_send_pack_buffer + assert_compiles '["ABC", "ABC", "ABC", "ABC"]', %q{ + def test(num, buffer) + [num].pack('C', buffer:) + end + buf = "" + [test(65, buf), test(66, buf), test(67, buf), buf] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_pack_buffer_redefined + assert_compiles '["b", "A"]', %q{ + class Array + alias_method :old_pack, :pack + def pack(fmt, buffer: nil) + old_pack(fmt, buffer: buffer) + "b" + end + end + + def test(num, buffer) + [num].pack('C', buffer:) + end + buf = "" + [test(65, buf), buf] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_hash + assert_compiles 'Integer', %q{ + def test(x) + [1, 2, x].hash + end + test(20).class + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_hash_redefined + assert_compiles '42', %q{ + Array.class_eval { def hash = 42 } + + def test(x) + [1, 2, x].hash + end + test(20) + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_max + assert_compiles '[20, 40]', %q{ + def test(a,b) = [a,b].max + [test(10, 20), test(40, 30)] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_max_redefined + assert_compiles '[60, 90]', %q{ + class Array + alias_method :old_max, :max + def max + old_max * 2 + end + end + + def test(a,b) = [a,b].max + [test(15, 30), test(45, 35)] + }, insns: [:opt_newarray_send], call_threshold: 1 + 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 + } + 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 + 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 + 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 "[{}, 5]", %q{ + def test = {}.freeze + result = [test] + class Hash + def freeze = 5 + end + result << test + }, insns: [:opt_hash_freeze], call_threshold: 1 + end + + def test_opt_hash_freeze_rewritten + assert_compiles "5", %q{ + class Hash + def freeze = 5 + end + def test = {}.freeze + test + }, insns: [:opt_hash_freeze], call_threshold: 1 + end + + def test_opt_aset_hash + assert_compiles '42', %q{ + def test(h, k, v) + h[k] = v + end + h = {} + test(h, :key, 42) + test(h, :key, 42) + h[:key] + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_opt_aset_hash_returns_value + assert_compiles '100', %q{ + def test(h, k, v) + h[k] = v + end + test({}, :key, 100) + test({}, :key, 100) + }, call_threshold: 2 + end + + def test_opt_aset_hash_string_key + assert_compiles '"bar"', %q{ + def test(h, k, v) + h[k] = v + end + h = {} + test(h, "foo", "bar") + test(h, "foo", "bar") + h["foo"] + }, call_threshold: 2 + end + + def test_opt_aset_hash_subclass + assert_compiles '42', %q{ + class MyHash < Hash; end + def test(h, k, v) + h[k] = v + end + h = MyHash.new + test(h, :key, 42) + test(h, :key, 42) + h[:key] + }, call_threshold: 2 + end + + def test_opt_aset_hash_too_few_args + assert_compiles '"ArgumentError"', %q{ + def test(h) + h.[]= 123 + rescue ArgumentError + "ArgumentError" + end + test({}) + test({}) }, call_threshold: 2 end + def test_opt_aset_hash_too_many_args + assert_compiles '"ArgumentError"', %q{ + def test(h) + h[:a, :b] = :c + rescue ArgumentError + "ArgumentError" + end + test({}) + test({}) + }, call_threshold: 2 + end + + def test_opt_ary_freeze + assert_compiles "[[], 5]", %q{ + def test = [].freeze + result = [test] + class Array + def freeze = 5 + end + result << test + }, insns: [:opt_ary_freeze], call_threshold: 1 + end + + def test_opt_ary_freeze_rewritten + assert_compiles "5", %q{ + class Array + def freeze = 5 + end + def test = [].freeze + test + }, insns: [:opt_ary_freeze], call_threshold: 1 + end + + def test_opt_str_freeze + assert_compiles "[\"\", 5]", %q{ + def test = ''.freeze + result = [test] + class String + def freeze = 5 + end + result << test + }, insns: [:opt_str_freeze], call_threshold: 1 + end + + def test_opt_str_freeze_rewritten + assert_compiles "5", %q{ + class String + def freeze = 5 + end + def test = ''.freeze + test + }, insns: [:opt_str_freeze], call_threshold: 1 + end + + def test_opt_str_uminus + assert_compiles "[\"\", 5]", %q{ + def test = -'' + result = [test] + class String + def -@ = 5 + end + result << test + }, insns: [:opt_str_uminus], call_threshold: 1 + end + + def test_opt_str_uminus_rewritten + assert_compiles "5", %q{ + class String + def -@ = 5 + end + def test = -'' + test + }, insns: [:opt_str_uminus], call_threshold: 1 + end + def test_new_array_empty assert_compiles '[]', %q{ def test = [] test - } + }, insns: [:newarray] end def test_new_array_nonempty @@ -219,6 +2220,272 @@ class TestZJIT < Test::Unit::TestCase } end + def test_array_fixnum_aref + assert_compiles '3', %q{ + def test(x) = [1,2,3][x] + test(2) + test(2) + }, call_threshold: 2, insns: [:opt_aref] + end + + def test_array_fixnum_aref_negative_index + assert_compiles '3', %q{ + def test(x) = [1,2,3][x] + test(-1) + test(-1) + }, call_threshold: 2, insns: [:opt_aref] + end + + def test_array_fixnum_aref_out_of_bounds_positive + assert_compiles 'nil', %q{ + def test(x) = [1,2,3][x] + test(10) + test(10) + }, call_threshold: 2, insns: [:opt_aref] + end + + def test_array_fixnum_aref_out_of_bounds_negative + assert_compiles 'nil', %q{ + def test(x) = [1,2,3][x] + test(-10) + test(-10) + }, call_threshold: 2, insns: [:opt_aref] + end + + def test_array_fixnum_aref_array_subclass + assert_compiles '3', %q{ + class MyArray < Array; end + def test(arr, idx) = arr[idx] + arr = MyArray[1,2,3] + test(arr, 2) + arr = MyArray[1,2,3] + test(arr, 2) + }, call_threshold: 2, insns: [:opt_aref] + end + + def test_array_aref_non_fixnum_index + assert_compiles 'TypeError', %q{ + def test(arr, idx) = arr[idx] + test([1,2,3], 1) + test([1,2,3], 1) + begin + test([1,2,3], "1") + rescue => e + e.class + end + }, call_threshold: 2 + end + + def test_array_fixnum_aset + assert_compiles '[1, 2, 7]', %q{ + def test(arr, idx) + arr[idx] = 7 + end + arr = [1,2,3] + test(arr, 2) + arr = [1,2,3] + test(arr, 2) + arr + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_array_fixnum_aset_returns_value + assert_compiles '7', %q{ + def test(arr, idx) + arr[idx] = 7 + end + test([1,2,3], 2) + test([1,2,3], 2) + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_array_fixnum_aset_out_of_bounds + assert_compiles '[1, 2, 3, nil, nil, 7]', %q{ + def test(arr) + arr[5] = 7 + end + arr = [1,2,3] + test(arr) + arr = [1,2,3] + test(arr) + arr + }, call_threshold: 2 + end + + def test_array_fixnum_aset_negative_index + assert_compiles '[1, 2, 7]', %q{ + def test(arr) + arr[-1] = 7 + end + arr = [1,2,3] + test(arr) + arr = [1,2,3] + test(arr) + arr + }, call_threshold: 2 + end + + def test_array_fixnum_aset_shared + assert_compiles '[10, 999, -1, -2]', %q{ + def test(arr, idx, val) + arr[idx] = val + end + arr = (0..50).to_a + test(arr, 0, -1) + test(arr, 1, -2) + shared = arr[10, 20] + test(shared, 0, 999) + [arr[10], shared[0], arr[0], arr[1]] + }, call_threshold: 2 + end + + def test_array_fixnum_aset_frozen + assert_compiles 'FrozenError', %q{ + def test(arr, idx, val) + arr[idx] = val + end + arr = [1,2,3] + test(arr, 1, 9) + test(arr, 1, 9) + arr.freeze + begin + test(arr, 1, 9) + rescue => e + e.class + end + }, call_threshold: 2 + end + + def test_array_fixnum_aset_array_subclass + assert_compiles '7', %q{ + class MyArray < Array; end + def test(arr, idx) + arr[idx] = 7 + end + arr = MyArray.new + test(arr, 0) + arr = MyArray.new + test(arr, 0) + arr[0] + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_array_aset_non_fixnum_index + assert_compiles 'TypeError', %q{ + def test(arr, idx) + arr[idx] = 7 + end + test([1,2,3], 0) + test([1,2,3], 0) + begin + test([1,2,3], "0") + rescue => e + e.class + end + }, call_threshold: 2 + end + + def test_empty_array_pop + assert_compiles 'nil', %q{ + def test(arr) = arr.pop + test([]) + test([]) + }, call_threshold: 2 + end + + def test_array_pop_no_arg + assert_compiles '42', %q{ + def test(arr) = arr.pop + test([32, 33, 42]) + test([32, 33, 42]) + }, call_threshold: 2 + end + + def test_array_pop_arg + assert_compiles '[33, 42]', %q{ + def test(arr) = arr.pop(2) + test([32, 33, 42]) + test([32, 33, 42]) + }, call_threshold: 2 + 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_new_range_fixnum_both_literals_inclusive + assert_compiles '1..2', %q{ + def test() + a = 2 + (1..a) + end + test; test + }, call_threshold: 2, insns: [:newrange] + end + + def test_new_range_fixnum_both_literals_exclusive + assert_compiles '1...2', %q{ + def test() + a = 2 + (1...a) + end + test; test + }, call_threshold: 2, insns: [:newrange] + end + + def test_new_range_fixnum_low_literal_inclusive + assert_compiles '1..3', %q{ + def test(a) + (1..a) + end + test(2); test(3) + }, call_threshold: 2, insns: [:newrange] + end + + def test_new_range_fixnum_low_literal_exclusive + assert_compiles '1...3', %q{ + def test(a) + (1...a) + end + test(2); test(3) + }, call_threshold: 2, insns: [:newrange] + end + + def test_new_range_fixnum_high_literal_inclusive + assert_compiles '3..10', %q{ + def test(a) + (a..10) + end + test(2); test(3) + }, call_threshold: 2, insns: [:newrange] + end + + def test_new_range_fixnum_high_literal_exclusive + assert_compiles '3...10', %q{ + def test(a) + (a...10) + end + test(2); test(3) + }, call_threshold: 2, insns: [:newrange] + end + def test_if assert_compiles '[0, nil]', %q{ def test(n) @@ -479,6 +2746,675 @@ class TestZJIT < Test::Unit::TestCase }, 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. + # Using assert_runs again due to register spill. + # TODO: It should be fixed by register spill support. + assert_runs '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_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_getinstancevariable_miss + assert_compiles '[1, 1, 4]', %q{ + class C + def foo + @foo + end + + def foo_then_bar + @foo = 1 + @bar = 2 + end + + def bar_then_foo + @bar = 3 + @foo = 4 + end + end + + o1 = C.new + o1.foo_then_bar + result = [] + result << o1.foo + result << o1.foo + o2 = C.new + o2.bar_then_foo + result << o2.foo + result + } + end + + def test_setinstancevariable + assert_compiles '1', %q{ + def test() = @foo = 1 + + test() + @foo + } + end + + def test_getclassvariable + assert_compiles '42', %q{ + class Foo + def self.test = @@x + end + + Foo.class_variable_set(:@@x, 42) + Foo.test() + } + end + + def test_getclassvariable_raises + assert_compiles '"uninitialized class variable @@x in Foo"', %q{ + class Foo + def self.test = @@x + end + + begin + Foo.test + rescue NameError => e + e.message + end + } + end + + def test_setclassvariable + assert_compiles '42', %q{ + class Foo + def self.test = @@x = 42 + end + + Foo.test() + Foo.class_variable_get(:@@x) + } + end + + def test_setclassvariable_raises + assert_compiles '"can\'t modify frozen Class: Foo"', %q{ + class Foo + def self.test = @@x = 42 + freeze + end + + begin + Foo.test + rescue FrozenError => e + e.message + end + } + 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_getivar + 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] + end + + def test_attr_accessor_setivar + assert_compiles '[5, 5]', %q{ + class C + attr_accessor :foo + + def initialize + @foo = 4 + end + end + + def test(c) + c.foo = 5 + c.foo + end + + c = C.new + [test(c), test(c)] + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_attr_writer + assert_compiles '[5, 5]', %q{ + class C + attr_writer :foo + + def initialize + @foo = 4 + end + + def get_foo = @foo + end + + def test(c) + c.foo = 5 + c.get_foo + end + c = C.new + [test(c), test(c)] + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_uncached_getconstant_path + assert_compiles RUBY_COPYRIGHT.dump, %q{ + def test = RUBY_COPYRIGHT + test + }, call_threshold: 1, insns: [:opt_getconstant_path] + end + + def test_expandarray_no_splat + assert_compiles '[3, 4]', %q{ + def test(o) + a, b = o + [a, b] + end + test [3, 4] + }, call_threshold: 1, insns: [:expandarray] + end + + def test_expandarray_splat + assert_compiles '[3, [4]]', %q{ + def test(o) + a, *b = o + [a, b] + end + test [3, 4] + }, call_threshold: 1, insns: [:expandarray] + end + + def test_expandarray_splat_post + assert_compiles '[3, [4], 5]', %q{ + def test(o) + a, *b, c = o + [a, b, c] + end + test [3, 4, 5] + }, call_threshold: 1, insns: [:expandarray] + end + + def test_getconstant_path_autoload + # A constant-referencing expression can run arbitrary code through Kernel#autoload. + Dir.mktmpdir('autoload') do |tmpdir| + autoload_path = File.join(tmpdir, 'test_getconstant_path_autoload.rb') + File.write(autoload_path, 'X = RUBY_COPYRIGHT') + + assert_compiles RUBY_COPYRIGHT.dump, %Q{ + Object.autoload(:X, #{File.realpath(autoload_path).inspect}) + def test = X + test + }, call_threshold: 1, insns: [:opt_getconstant_path] + 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'", + "-e:3:in 'Object#entry'", + "-e:5:in 'block in <main>'", + "-e:6:in '<main>'", + ] + assert_compiles backtrace.inspect, %q{ + def jit_frame2 = caller # 1 + def jit_frame1 = jit_frame2 # 2 + def entry = jit_frame1 # 3 + entry # profile send # 4 + entry # 5 + }, call_threshold: 2 + end + + def test_bop_invalidation + assert_compiles '100', %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_method_raise + assert_compiles '[nil, nil, nil]', %q{ + class C + def assert_equal expected, actual + if expected != actual + raise "NO" + end + end + + def test_defined_method + assert_equal(nil, defined?("x".reverse(1).reverse)) + end + end + + c = C.new + result = [] + result << c.test_defined_method + result << c.test_defined_method + result << c.test_defined_method + result + } + 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. + + assert_compiles '[nil, nil, "yield"]', %q{ + def test + yield_self { yield_self { defined?(yield) } } + end + + [test, test, test{}] + }, call_threshold: 2 + end + + def test_block_given_p + assert_compiles "false", "block_given?" + assert_compiles '[false, false, true]', %q{ + def test = block_given? + [test, test, test{}] + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_block_given_p_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. + + assert_compiles '[false, false, true]', %q{ + def test + yield_self { yield_self { block_given? } } + end + + [test, test, test{}] + }, call_threshold: 2 + 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 @@ -492,58 +3428,1111 @@ class TestZJIT < Test::Unit::TestCase end end + def test_require_rubygems + assert_runs 'true', %q{ + require 'rubygems' + }, call_threshold: 2 + 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_availability + assert_runs '[true, true]', %q{ + def test = 1 + test + [ + RubyVM::ZJIT.stats[:zjit_insn_count] > 0, + RubyVM::ZJIT.stats(:zjit_insn_count) > 0, + ] + }, stats: true + end + + def test_stats_consistency + assert_runs '[]', %q{ + def test = 1 + test # increment some counters + + 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 + }, stats: true + end + + def test_reset_stats + assert_runs 'true', %q{ + def test = 1 + 100.times { test } + + # Get initial stats and verify they're non-zero + initial_stats = RubyVM::ZJIT.stats + + # Reset the stats + RubyVM::ZJIT.reset_stats! + + # Get stats after reset + reset_stats = RubyVM::ZJIT.stats + + [ + # 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_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 jit_call(flag) + if flag + profile + end + end + + def entry(flag) + jit_call(flag) + end + + [entry(false), entry(false), entry(true)] + }, call_threshold: 2 + end + + def test_bop_redefined + 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_redefined_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 + end + + # ZJIT currently only generates a MethodRedefined patch point when the method + # is called on the top-level self. + def test_method_redefined_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 + + result2 = test + + [result1, result2] + }, call_threshold: 2 + end + + def test_method_redefined_with_module + assert_runs '["original", "redefined"]', %q{ + module Foo + def self.foo = "original" + end + + def test = Foo.foo + test + result1 = test + + def Foo.foo = "redefined" + result2 = test + + [result1, result2] + }, call_threshold: 2 + end + + def test_module_name_with_guard_passes + assert_compiles '"Integer"', %q{ + def test(mod) + mod.name + end + + test(String) + test(Integer) + }, call_threshold: 2 + 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 + end + + def test_objtostring_calls_to_s_on_non_strings + assert_compiles '["foo", "foo"]', %q{ + results = [] + + class Foo + def to_s + "foo" + end + end + + def test(str) + "#{str}" + end + + results << test(Foo.new) + results << test(Foo.new) + + results + } + 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 + end + + def test(foo) + "#{foo}" + end + + results << test("foo") + results << test("foo") + + results + } + 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 + end + + foo = StringSubclass.new("foo") + + def test(str) + "#{str}" + end + + results << test(foo) + results << test(foo) + + results + } + end + + def test_objtostring_profiled_string_fastpath + assert_compiles '"foo"', %q{ + def test(str) + "#{str}" + end + test('foo'); test('foo') # profile as string + }, call_threshold: 2 + end + + def test_objtostring_profiled_string_subclass_fastpath + assert_compiles '"foo"', %q{ + class MyString < String; end + + def test(str) + "#{str}" + end + + foo = MyString.new("foo") + test(foo); test(foo) # still profiles as string + }, call_threshold: 2 + end + + def test_objtostring_profiled_string_fastpath_exits_on_nonstring + assert_compiles '"1"', %q{ + def test(str) + "#{str}" + end + + test('foo') # profile as string + test(1) + }, call_threshold: 2 + end + + def test_objtostring_profiled_nonstring_calls_to_s + assert_compiles '"[1, 2, 3]"', %q{ + def test(str) + "#{str}" + end + + test([1,2,3]); # profile as nonstring + test([1,2,3]); + }, call_threshold: 2 + end + + def test_objtostring_profiled_nonstring_guard_exits_when_string + assert_compiles '"foo"', %q{ + def test(str) + "#{str}" + end + + test([1,2,3]); # profiles as nonstring + test('foo'); + }, call_threshold: 2 + end + + def test_string_bytesize_with_guard + assert_compiles '5', %q{ + def test(str) + str.bytesize + end + + test('hello') + test('world') + }, call_threshold: 2 + end + + def test_string_bytesize_multibyte + assert_compiles '4', %q{ + def test(s) + s.bytesize + end + + test("💎") + }, 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] + end + + def test_string_concat_empty + assert_compiles '""', %q{ + def test = "#{}" + + test + }, insns: [:concatstrings] + end + + def test_regexp_interpolation + assert_compiles '/123/', %q{ + def test = /#{1}#{2}#{3}/ + + test + }, insns: [:toregexp] + 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 + 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) + } + end + + def test_ivar_attr_reader_optimization_with_multi_ractor_mode + assert_compiles '42', %q{ + class Foo + class << self + attr_accessor :bar + + def get_bar + bar + rescue Ractor::IsolationError + 42 + end + end + end + + Foo.bar = [] # needs to be a ractor unshareable object + + def test + Foo.get_bar + end + + test + test + + Ractor.new { test }.value + }, call_threshold: 2 + end + + def test_ivar_get_with_multi_ractor_mode + assert_compiles '42', %q{ + class Foo + def self.set_bar + @bar = [] # needs to be a ractor unshareable object + end + + def self.bar + @bar + rescue Ractor::IsolationError + 42 + end + end + + Foo.set_bar + + def test + Foo.bar + end + + test + test + + Ractor.new { test }.value + }, call_threshold: 2 + end + + def test_ivar_get_with_already_multi_ractor_mode + assert_compiles '42', %q{ + class Foo + def self.set_bar + @bar = [] # needs to be a ractor unshareable object + end + + def self.bar + @bar + rescue Ractor::IsolationError + 42 + end + end + + Foo.set_bar + r = Ractor.new { + Ractor.receive + Foo.bar + } + + Foo.bar + Foo.bar + + r << :go + r.value + }, call_threshold: 2 + end + + def test_ivar_set_with_multi_ractor_mode + assert_compiles '42', %q{ + class Foo + def self.bar + _foo = 1 + _bar = 2 + begin + @bar = _foo + _bar + rescue Ractor::IsolationError + 42 + end + end + end + + def test + Foo.bar + end + + test + test + + Ractor.new { test }.value + } + end + + def test_struct_set + assert_compiles '[42, 42, :frozen_error]', %q{ + C = Struct.new(:foo).new(1) + + def test + C.foo = Object.new + 42 + end + + r = [test, test] + C.freeze + r << begin + test + rescue FrozenError + :frozen_error + end + }, call_threshold: 2 + end + + def test_global_tracepoint + assert_compiles 'true', %q{ + def foo = 1 + + foo + foo + + called = false + + tp = TracePoint.new(:return) { |event| + if event.method_id == :foo + called = true + end + } + tp.enable do + foo + end + called + } + end + + def test_local_tracepoint + assert_compiles 'true', %q{ + def foo = 1 + + foo + foo + + called = false + + tp = TracePoint.new(:return) { |_| called = true } + tp.enable(target: method(:foo)) do + foo + end + called + } + 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 + + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + @tp.disable; __LINE__ + end + + line = events.compiled(events) + events[0][-1] = (events[0][-1] == line) + + 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_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 + + # Stay in generated code while enabling tracing + def events.compiled(obj) + String(obj) + __LINE__ + end + + line = events.compiled(events) + events[0] = (events[0] == line) + + 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_opt_case_dispatch + assert_compiles '[true, false]', %q{ + def test(x) + case x + when :foo + true + else + false + end + end + + results = [] + results << test(:foo) + results << test(1) + results + }, insns: [:opt_case_dispatch] + end + + def test_stack_overflow + assert_compiles 'nil', %q{ + def recurse(n) + return if n == 0 + recurse(n-1) + nil # no tail call + end + + recurse(2) + recurse(2) + begin + recurse(20_000) + rescue SystemStackError + # Not asserting an exception is raised here since main + # thread stack size is environment-sensitive. Only + # that we don't crash or infinite loop. + end + }, call_threshold: 2 + end + + def test_invokeblock + assert_compiles '42', %q{ + def test + yield + end + test { 42 } + }, insns: [:invokeblock] + end + + def test_invokeblock_with_args + assert_compiles '3', %q{ + def test(x, y) + yield x, y + end + test(1, 2) { |a, b| a + b } + }, insns: [:invokeblock] + end + + def test_invokeblock_no_block_given + assert_compiles ':error', %q{ + def test + yield rescue :error + end + test + }, insns: [:invokeblock] + end + + def test_invokeblock_multiple_yields + assert_compiles "[1, 2, 3]", %q{ + results = [] + def test + yield 1 + yield 2 + yield 3 + end + test { |x| results << x } + results + }, insns: [:invokeblock] + end + + def test_ccall_variadic_with_multiple_args + assert_compiles "[1, 2, 3]", %q{ + def test + a = [] + a.push(1, 2, 3) + a + end + + test + test + }, insns: [:opt_send_without_block] + end + + def test_ccall_variadic_with_no_args + assert_compiles "[1]", %q{ + def test + a = [1] + a.push + end + + test + test + }, insns: [:opt_send_without_block] + end + + def test_ccall_variadic_with_no_args_causing_argument_error + assert_compiles ":error", %q{ + def test + format + rescue ArgumentError + :error + end + + test + test + }, insns: [:opt_send_without_block] + end + + def test_allocating_in_hir_c_method_is + assert_compiles ":k", %q{ + # Put opt_new in a frame JIT code sets up that doesn't set cfp->pc + def a(f) = test(f) + def test(f) = (f.new if f) + # A parallel couple methods that will set PC at the same stack height + def second = third + def third = nil + + a(nil) + a(nil) + + class Foo + def self.new = :k + end + + second + + a(Foo) + }, call_threshold: 2, insns: [:opt_new] + end + + def test_singleton_class_invalidation_annotated_ccall + assert_compiles '[false, true]', %q{ + def define_singleton(obj, define) + if define + # Wrap in C method frame to avoid exiting JIT on defineclass + [nil].reverse_each do + class << obj + def ==(_) + true + end + end + end + end + false + end + + def test(define) + obj = BasicObject.new + # This == call gets compiled to a CCall + obj == define_singleton(obj, define) + end + + result = [] + result << test(false) # Compiles BasicObject#== + result << test(true) # Should use singleton#== now + result + }, call_threshold: 2 + end + + def test_singleton_class_invalidation_optimized_variadic_ccall + assert_compiles '[1, 1000]', %q{ + def define_singleton(arr, define) + if define + # Wrap in C method frame to avoid exiting JIT on defineclass + [nil].reverse_each do + class << arr + def push(x) + super(x * 1000) + end + end + end + end + 1 + end + + def test(define) + arr = [] + val = define_singleton(arr, define) + arr.push(val) # This CCall should be invalidated if singleton was defined + arr[0] + end + + result = [] + result << test(false) # Compiles Array#push as CCall + result << test(true) # Singleton defined, CCall should be invalidated + result + }, call_threshold: 2 + 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 + + def tree(depth) + # This duparray is our leaf-gc target. + return [nil, nil] unless depth > 0 + + # Modify the local and pass it to the following calls. + depth -= 1 + [tree(depth), tree(depth)] + end + + def test + GC.stress = true + 2.times do + t = tree(11) + check(*t) + end + :ok + end + + test + }, call_threshold: 14, num_profiles: 5 + end + private # Assert that every method call in `test_script` can be compiled by ZJIT # at a given call_threshold - def assert_compiles(expected, test_script, **opts) + def assert_compiles(expected, test_script, insns: [], **opts) + assert_runs(expected, test_script, insns:, assert_compiles: true, **opts) + end + + # Assert that `test_script` runs successfully with ZJIT enabled. + # Unlike `assert_compiles`, `assert_runs(assert_compiles: false)` + # 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 - _test_proc = -> { - RubyVM::ZJIT.assert_compiles - #{test_script} + 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(#{disasm_method.inspect})).to_a" + end} } - result = _test_proc.call - IO.open(#{pipe_fd}).write(result.inspect) + IO.open(#{pipe_fd}).write(Marshal.dump(result)) RUBY - status, out, err, actual = eval_with_jit(script, pipe_fd:, **opts) + out, err, status, result = eval_with_jit(script, pipe_fd:, **opts) + 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 + 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. " \ + "Make sure to put code directly under the '#{disasm_method}' method." + ) + iseq_insns = iseq.last - assert_equal expected, actual + expected_insns = Set.new(insns) + iseq_insns.each do + next unless it.is_a?(Array) + expected_insns.delete(it.first) + end + assert(expected_insns.empty?, -> { "Not present in ISeq: #{expected_insns.to_a}" }) + end end # Run a Ruby process with ZJIT options and a pipe for writing test results - def eval_with_jit(script, call_threshold: 1, num_profiles: 1, timeout: 1000, pipe_fd:, debug: true) - args = [ - "--disable-gems", - "--zjit-call-threshold=#{call_threshold}", - "--zjit-num-profiles=#{num_profiles}", - ] - args << "--zjit-debug" if debug + def eval_with_jit( + script, + call_threshold: 1, + num_profiles: 1, + zjit: true, + stats: false, + debug: true, + allowed_iseqs: nil, + timeout: 1000, + pipe_fd: nil + ) + args = ["--disable-gems"] + if zjit + args << "--zjit-call-threshold=#{call_threshold}" + args << "--zjit-num-profiles=#{num_profiles}" + 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") + jitlist.write(allowed_iseqs) + jitlist.close + args << "--zjit-allowed-iseqs=#{jitlist.path}" + 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 + 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 - 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 ensure pipe_reader&.kill pipe_reader&.join(timeout) pipe_r&.close pipe_w&.close + 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) |
