diff options
Diffstat (limited to 'test/ruby')
143 files changed, 26923 insertions, 3537 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_case_comprehensive.rb b/test/ruby/enc/test_case_comprehensive.rb index de18ac865c..b812b88b83 100644 --- a/test/ruby/enc/test_case_comprehensive.rb +++ b/test/ruby/enc/test_case_comprehensive.rb @@ -161,15 +161,14 @@ TestComprehensiveCaseMapping.data_files_available? and class TestComprehensiveC end end - def self.generate_case_mapping_tests(encoding) + def self.generate_single_byte_case_mapping_tests(encoding) all_tests - # preselect codepoints to speed up testing for small encodings - codepoints = @@codepoints.select do |code| + # precalculate codepoints to speed up testing for small encodings + codepoints = [] + (0..255).each do |cp| begin - code.encode(encoding) - true - rescue Encoding::UndefinedConversionError - false + codepoints << cp.chr(encoding).encode('UTF-8') + rescue Encoding::UndefinedConversionError, RangeError end end all_tests.each do |test| @@ -264,23 +263,23 @@ TestComprehensiveCaseMapping.data_files_available? and class TestComprehensiveC end end - generate_case_mapping_tests 'US-ASCII' - generate_case_mapping_tests 'ASCII-8BIT' - generate_case_mapping_tests 'ISO-8859-1' - generate_case_mapping_tests 'ISO-8859-2' - generate_case_mapping_tests 'ISO-8859-3' - generate_case_mapping_tests 'ISO-8859-4' - generate_case_mapping_tests 'ISO-8859-5' - generate_case_mapping_tests 'ISO-8859-6' - generate_case_mapping_tests 'ISO-8859-7' - generate_case_mapping_tests 'ISO-8859-8' - generate_case_mapping_tests 'ISO-8859-9' - generate_case_mapping_tests 'ISO-8859-10' - generate_case_mapping_tests 'ISO-8859-11' - generate_case_mapping_tests 'ISO-8859-13' - generate_case_mapping_tests 'ISO-8859-14' - generate_case_mapping_tests 'ISO-8859-15' - generate_case_mapping_tests 'ISO-8859-16' + generate_single_byte_case_mapping_tests 'US-ASCII' + generate_single_byte_case_mapping_tests 'ASCII-8BIT' + generate_single_byte_case_mapping_tests 'ISO-8859-1' + generate_single_byte_case_mapping_tests 'ISO-8859-2' + generate_single_byte_case_mapping_tests 'ISO-8859-3' + generate_single_byte_case_mapping_tests 'ISO-8859-4' + generate_single_byte_case_mapping_tests 'ISO-8859-5' + generate_single_byte_case_mapping_tests 'ISO-8859-6' + generate_single_byte_case_mapping_tests 'ISO-8859-7' + generate_single_byte_case_mapping_tests 'ISO-8859-8' + generate_single_byte_case_mapping_tests 'ISO-8859-9' + generate_single_byte_case_mapping_tests 'ISO-8859-10' + generate_single_byte_case_mapping_tests 'ISO-8859-11' + generate_single_byte_case_mapping_tests 'ISO-8859-13' + generate_single_byte_case_mapping_tests 'ISO-8859-14' + generate_single_byte_case_mapping_tests 'ISO-8859-15' + generate_single_byte_case_mapping_tests 'ISO-8859-16' generate_ascii_only_case_mapping_tests 'KOI8-R' generate_ascii_only_case_mapping_tests 'KOI8-U' generate_ascii_only_case_mapping_tests 'Big5' @@ -291,14 +290,14 @@ TestComprehensiveCaseMapping.data_files_available? and class TestComprehensiveC generate_ascii_only_case_mapping_tests 'GBK' generate_ascii_only_case_mapping_tests 'Shift_JIS' generate_ascii_only_case_mapping_tests 'Windows-31J' - generate_case_mapping_tests 'Windows-1250' - generate_case_mapping_tests 'Windows-1251' - generate_case_mapping_tests 'Windows-1252' - generate_case_mapping_tests 'Windows-1253' - generate_case_mapping_tests 'Windows-1254' - generate_case_mapping_tests 'Windows-1255' + generate_single_byte_case_mapping_tests 'Windows-1250' + generate_single_byte_case_mapping_tests 'Windows-1251' + generate_single_byte_case_mapping_tests 'Windows-1252' + generate_single_byte_case_mapping_tests 'Windows-1253' + generate_single_byte_case_mapping_tests 'Windows-1254' + generate_single_byte_case_mapping_tests 'Windows-1255' generate_ascii_only_case_mapping_tests 'Windows-1256' - generate_case_mapping_tests 'Windows-1257' + generate_single_byte_case_mapping_tests 'Windows-1257' generate_unicode_case_mapping_tests 'UTF-8' generate_unicode_case_mapping_tests 'UTF-16BE' generate_unicode_case_mapping_tests 'UTF-16LE' diff --git a/test/ruby/enc/test_case_mapping.rb b/test/ruby/enc/test_case_mapping.rb index 31acdc4331..a7d1ed0d16 100644 --- a/test/ruby/enc/test_case_mapping.rb +++ b/test/ruby/enc/test_case_mapping.rb @@ -47,7 +47,7 @@ class TestCaseMappingPreliminary < Test::Unit::TestCase # different properties; careful: roundtrip isn't always guaranteed def check_swapcase_properties(expected, start, *flags) assert_equal expected, start.swapcase(*flags) - temp = start + temp = +start assert_equal expected, temp.swapcase!(*flags) assert_equal start, start.swapcase(*flags).swapcase(*flags) assert_equal expected, expected.swapcase(*flags).swapcase(*flags) @@ -61,10 +61,10 @@ class TestCaseMappingPreliminary < Test::Unit::TestCase end def test_invalid - assert_raise(ArgumentError, "Should not be possible to upcase invalid string.") { "\xEB".force_encoding('UTF-8').upcase } - assert_raise(ArgumentError, "Should not be possible to downcase invalid string.") { "\xEB".force_encoding('UTF-8').downcase } - assert_raise(ArgumentError, "Should not be possible to capitalize invalid string.") { "\xEB".force_encoding('UTF-8').capitalize } - assert_raise(ArgumentError, "Should not be possible to swapcase invalid string.") { "\xEB".force_encoding('UTF-8').swapcase } + assert_raise(ArgumentError, "Should not be possible to upcase invalid string.") { "\xEB".dup.force_encoding('UTF-8').upcase } + assert_raise(ArgumentError, "Should not be possible to downcase invalid string.") { "\xEB".dup.force_encoding('UTF-8').downcase } + assert_raise(ArgumentError, "Should not be possible to capitalize invalid string.") { "\xEB".dup.force_encoding('UTF-8').capitalize } + assert_raise(ArgumentError, "Should not be possible to swapcase invalid string.") { "\xEB".dup.force_encoding('UTF-8').swapcase } end def test_general diff --git a/test/ruby/enc/test_case_options.rb b/test/ruby/enc/test_case_options.rb index e9bf50fcfc..e9c81d804e 100644 --- a/test/ruby/enc/test_case_options.rb +++ b/test/ruby/enc/test_case_options.rb @@ -19,7 +19,7 @@ class TestCaseOptions < Test::Unit::TestCase def assert_raise_both_types(*options) assert_raise_functional_operations 'a', *options - assert_raise_bang_operations 'a', *options + assert_raise_bang_operations(+'a', *options) assert_raise_functional_operations :a, *options end @@ -51,7 +51,7 @@ class TestCaseOptions < Test::Unit::TestCase def assert_okay_both_types(*options) assert_okay_functional_operations 'a', *options - assert_okay_bang_operations 'a', *options + assert_okay_bang_operations(+'a', *options) assert_okay_functional_operations :a, *options end @@ -69,10 +69,10 @@ class TestCaseOptions < Test::Unit::TestCase assert_raise(ArgumentError) { 'a'.upcase :fold } assert_raise(ArgumentError) { 'a'.capitalize :fold } assert_raise(ArgumentError) { 'a'.swapcase :fold } - assert_nothing_raised { 'a'.downcase! :fold } - assert_raise(ArgumentError) { 'a'.upcase! :fold } - assert_raise(ArgumentError) { 'a'.capitalize! :fold } - assert_raise(ArgumentError) { 'a'.swapcase! :fold } + assert_nothing_raised { 'a'.dup.downcase! :fold } + assert_raise(ArgumentError) { 'a'.dup.upcase! :fold } + assert_raise(ArgumentError) { 'a'.dup.capitalize! :fold } + assert_raise(ArgumentError) { 'a'.dup.swapcase! :fold } assert_nothing_raised { :a.downcase :fold } assert_raise(ArgumentError) { :a.upcase :fold } assert_raise(ArgumentError) { :a.capitalize :fold } 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/rjit/test_assembler.rb b/test/ruby/rjit/test_assembler.rb deleted file mode 100644 index 45805115a5..0000000000 --- a/test/ruby/rjit/test_assembler.rb +++ /dev/null @@ -1,350 +0,0 @@ -require 'test/unit' -require_relative '../../lib/jit_support' - -return unless JITSupport.rjit_supported? -return unless RubyVM::RJIT.enabled? -return unless RubyVM::RJIT::C.HAVE_LIBCAPSTONE - -require 'stringio' -require 'ruby_vm/rjit/assembler' - -module RubyVM::RJIT - class TestAssembler < Test::Unit::TestCase - MEM_SIZE = 16 * 1024 - - def setup - @mem_block ||= C.mmap(MEM_SIZE) - @cb = CodeBlock.new(mem_block: @mem_block, mem_size: MEM_SIZE) - end - - def test_add - asm = Assembler.new - asm.add([:rcx], 1) # ADD r/m64, imm8 (Mod 00: [reg]) - asm.add(:rax, 0x7f) # ADD r/m64, imm8 (Mod 11: reg) - asm.add(:rbx, 0x7fffffff) # ADD r/m64 imm32 (Mod 11: reg) - asm.add(:rsi, :rdi) # ADD r/m64, r64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: add qword ptr [rcx], 1 - 0x4: add rax, 0x7f - 0x8: add rbx, 0x7fffffff - 0xf: add rsi, rdi - EOS - end - - def test_and - asm = Assembler.new - asm.and(:rax, 0) # AND r/m64, imm8 (Mod 11: reg) - asm.and(:rbx, 0x7fffffff) # AND r/m64, imm32 (Mod 11: reg) - asm.and(:rcx, [:rdi, 8]) # AND r64, r/m64 (Mod 01: [reg]+disp8) - assert_compile(asm, <<~EOS) - 0x0: and rax, 0 - 0x4: and rbx, 0x7fffffff - 0xb: and rcx, qword ptr [rdi + 8] - EOS - end - - def test_call - asm = Assembler.new - asm.call(rel32(0xff)) # CALL rel32 - asm.call(:rax) # CALL r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: call 0xff - 0x5: call rax - EOS - end - - def test_cmove - asm = Assembler.new - asm.cmove(:rax, :rcx) # CMOVE r64, r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: cmove rax, rcx - EOS - end - - def test_cmovg - asm = Assembler.new - asm.cmovg(:rbx, :rdi) # CMOVG r64, r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: cmovg rbx, rdi - EOS - end - - def test_cmovge - asm = Assembler.new - asm.cmovge(:rsp, :rbp) # CMOVGE r64, r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: cmovge rsp, rbp - EOS - end - - def test_cmovl - asm = Assembler.new - asm.cmovl(:rdx, :rsp) # CMOVL r64, r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: cmovl rdx, rsp - EOS - end - - def test_cmovle - asm = Assembler.new - asm.cmovle(:rax, :rax) # CMOVLE r64, r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: cmovle rax, rax - EOS - end - - def test_cmovnz - asm = Assembler.new - asm.cmovnz(:rax, :rbx) # CMOVNZ r64, r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) # cmovne == cmovnz - 0x0: cmovne rax, rbx - EOS - end - - def test_cmovz - asm = Assembler.new - asm.cmovz(:rax, :rbx) # CMOVZ r64, r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) # cmove == cmovz - 0x0: cmove rax, rbx - EOS - end - - def test_cmp - asm = Assembler.new - asm.cmp(BytePtr[:rax, 8], 8) # CMP r/m8, imm8 (Mod 01: [reg]+disp8) - asm.cmp(DwordPtr[:rax, 8], 0x100) # CMP r/m32, imm32 (Mod 01: [reg]+disp8) - asm.cmp([:rax, 8], 8) # CMP r/m64, imm8 (Mod 01: [reg]+disp8) - asm.cmp([:rax, 0x100], 8) # CMP r/m64, imm8 (Mod 10: [reg]+disp32) - asm.cmp(:rax, 8) # CMP r/m64, imm8 (Mod 11: reg) - asm.cmp(:rax, 0x100) # CMP r/m64, imm32 (Mod 11: reg) - asm.cmp([:rax, 8], :rbx) # CMP r/m64, r64 (Mod 01: [reg]+disp8) - asm.cmp([:rax, -0x100], :rbx) # CMP r/m64, r64 (Mod 10: [reg]+disp32) - asm.cmp(:rax, :rbx) # CMP r/m64, r64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: cmp byte ptr [rax + 8], 8 - 0x4: cmp dword ptr [rax + 8], 0x100 - 0xb: cmp qword ptr [rax + 8], 8 - 0x10: cmp qword ptr [rax + 0x100], 8 - 0x18: cmp rax, 8 - 0x1c: cmp rax, 0x100 - 0x23: cmp qword ptr [rax + 8], rbx - 0x27: cmp qword ptr [rax - 0x100], rbx - 0x2e: cmp rax, rbx - EOS - end - - def test_jbe - asm = Assembler.new - asm.jbe(rel32(0xff)) # JBE rel32 - assert_compile(asm, <<~EOS) - 0x0: jbe 0xff - EOS - end - - def test_je - asm = Assembler.new - asm.je(rel32(0xff)) # JE rel32 - assert_compile(asm, <<~EOS) - 0x0: je 0xff - EOS - end - - def test_jl - asm = Assembler.new - asm.jl(rel32(0xff)) # JL rel32 - assert_compile(asm, <<~EOS) - 0x0: jl 0xff - EOS - end - - def test_jmp - asm = Assembler.new - label = asm.new_label('label') - asm.jmp(label) # JZ rel8 - asm.write_label(label) - asm.jmp(rel32(0xff)) # JMP rel32 - asm.jmp([:rax, 8]) # JMP r/m64 (Mod 01: [reg]+disp8) - asm.jmp(:rax) # JMP r/m64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: jmp 2 - 0x2: jmp 0xff - 0x7: jmp qword ptr [rax + 8] - 0xa: jmp rax - EOS - end - - def test_jne - asm = Assembler.new - asm.jne(rel32(0xff)) # JNE rel32 - assert_compile(asm, <<~EOS) - 0x0: jne 0xff - EOS - end - - def test_jnz - asm = Assembler.new - asm.jnz(rel32(0xff)) # JNZ rel32 - assert_compile(asm, <<~EOS) - 0x0: jne 0xff - EOS - end - - def test_jo - asm = Assembler.new - asm.jo(rel32(0xff)) # JO rel32 - assert_compile(asm, <<~EOS) - 0x0: jo 0xff - EOS - end - - def test_jz - asm = Assembler.new - asm.jz(rel32(0xff)) # JZ rel32 - assert_compile(asm, <<~EOS) - 0x0: je 0xff - EOS - end - - def test_lea - asm = Assembler.new - asm.lea(:rax, [:rax, 8]) # LEA r64,m (Mod 01: [reg]+disp8) - asm.lea(:rax, [:rax, 0xffff]) # LEA r64,m (Mod 10: [reg]+disp32) - assert_compile(asm, <<~EOS) - 0x0: lea rax, [rax + 8] - 0x4: lea rax, [rax + 0xffff] - EOS - end - - def test_mov - asm = Assembler.new - asm.mov(:eax, DwordPtr[:rbx, 8]) # MOV r32 r/m32 (Mod 01: [reg]+disp8) - asm.mov(:eax, 0x100) # MOV r32, imm32 (Mod 11: reg) - asm.mov(:rax, [:rbx]) # MOV r64, r/m64 (Mod 00: [reg]) - asm.mov(:rax, [:rbx, 8]) # MOV r64, r/m64 (Mod 01: [reg]+disp8) - asm.mov(:rax, [:rbx, 0x100]) # MOV r64, r/m64 (Mod 10: [reg]+disp32) - asm.mov(:rax, :rbx) # MOV r64, r/m64 (Mod 11: reg) - asm.mov(:rax, 0x100) # MOV r/m64, imm32 (Mod 11: reg) - asm.mov(:rax, 0x100000000) # MOV r64, imm64 - asm.mov(DwordPtr[:rax, 8], 0x100) # MOV r/m32, imm32 (Mod 01: [reg]+disp8) - asm.mov([:rax], 0x100) # MOV r/m64, imm32 (Mod 00: [reg]) - asm.mov([:rax], :rbx) # MOV r/m64, r64 (Mod 00: [reg]) - asm.mov([:rax, 8], 0x100) # MOV r/m64, imm32 (Mod 01: [reg]+disp8) - asm.mov([:rax, 8], :rbx) # MOV r/m64, r64 (Mod 01: [reg]+disp8) - asm.mov([:rax, 0x100], 0x100) # MOV r/m64, imm32 (Mod 10: [reg]+disp32) - asm.mov([:rax, 0x100], :rbx) # MOV r/m64, r64 (Mod 10: [reg]+disp32) - assert_compile(asm, <<~EOS) - 0x0: mov eax, dword ptr [rbx + 8] - 0x3: mov eax, 0x100 - 0x8: mov rax, qword ptr [rbx] - 0xb: mov rax, qword ptr [rbx + 8] - 0xf: mov rax, qword ptr [rbx + 0x100] - 0x16: mov rax, rbx - 0x19: mov rax, 0x100 - 0x20: movabs rax, 0x100000000 - 0x2a: mov dword ptr [rax + 8], 0x100 - 0x31: mov qword ptr [rax], 0x100 - 0x38: mov qword ptr [rax], rbx - 0x3b: mov qword ptr [rax + 8], 0x100 - 0x43: mov qword ptr [rax + 8], rbx - 0x47: mov qword ptr [rax + 0x100], 0x100 - 0x52: mov qword ptr [rax + 0x100], rbx - EOS - end - - def test_or - asm = Assembler.new - asm.or(:rax, 0) # OR r/m64, imm8 (Mod 11: reg) - asm.or(:rax, 0xffff) # OR r/m64, imm32 (Mod 11: reg) - asm.or(:rax, [:rbx, 8]) # OR r64, r/m64 (Mod 01: [reg]+disp8) - assert_compile(asm, <<~EOS) - 0x0: or rax, 0 - 0x4: or rax, 0xffff - 0xb: or rax, qword ptr [rbx + 8] - EOS - end - - def test_push - asm = Assembler.new - asm.push(:rax) # PUSH r64 - assert_compile(asm, <<~EOS) - 0x0: push rax - EOS - end - - def test_pop - asm = Assembler.new - asm.pop(:rax) # POP r64 - assert_compile(asm, <<~EOS) - 0x0: pop rax - EOS - end - - def test_ret - asm = Assembler.new - asm.ret # RET - assert_compile(asm, "0x0: ret \n") - end - - def test_sar - asm = Assembler.new - asm.sar(:rax, 0) # SAR r/m64, imm8 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: sar rax, 0 - EOS - end - - def test_sub - asm = Assembler.new - asm.sub(:rax, 8) # SUB r/m64, imm8 (Mod 11: reg) - asm.sub(:rax, :rbx) # SUB r/m64, r64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: sub rax, 8 - 0x4: sub rax, rbx - EOS - end - - def test_test - asm = Assembler.new - asm.test(BytePtr[:rax, 8], 16) # TEST r/m8*, imm8 (Mod 01: [reg]+disp8) - asm.test([:rax, 8], 8) # TEST r/m64, imm32 (Mod 01: [reg]+disp8) - asm.test([:rax, 0xffff], 0xffff) # TEST r/m64, imm32 (Mod 10: [reg]+disp32) - asm.test(:rax, 0xffff) # TEST r/m64, imm32 (Mod 11: reg) - asm.test(:eax, :ebx) # TEST r/m32, r32 (Mod 11: reg) - asm.test(:rax, :rbx) # TEST r/m64, r64 (Mod 11: reg) - assert_compile(asm, <<~EOS) - 0x0: test byte ptr [rax + 8], 0x10 - 0x4: test qword ptr [rax + 8], 8 - 0xc: test qword ptr [rax + 0xffff], 0xffff - 0x17: test rax, 0xffff - 0x1e: test eax, ebx - 0x20: test rax, rbx - EOS - end - - private - - def rel32(offset) - @cb.write_addr + 0xff - end - - def assert_compile(asm, expected) - actual = compile(asm) - assert_equal expected, actual, "---\n#{actual}---" - end - - def compile(asm) - start_addr = @cb.write_addr - @cb.write(asm) - end_addr = @cb.write_addr - - io = StringIO.new - @cb.dump_disasm(start_addr, end_addr, io:, color: false, test: true) - io.seek(0) - disasm = io.read - - disasm.gsub!(/^ /, '') - disasm.sub!(/\n\z/, '') - disasm - end - end -end 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_alias.rb b/test/ruby/test_alias.rb index 0d33cb993c..539cd49488 100644 --- a/test/ruby/test_alias.rb +++ b/test/ruby/test_alias.rb @@ -292,4 +292,53 @@ class TestAlias < Test::Unit::TestCase end end; end + + def test_alias_complemented_method + assert_in_out_err(%w[-w], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + module M + def foo = 1 + self.extend M + end + + 3.times{|i| + module M + alias foo2 foo + remove_method :foo + def foo = 2 + ensure + remove_method :foo + alias foo foo2 + remove_method :foo2 + end + + M.foo + + original_foo = M.method(:foo) + + M.class_eval do + remove_method :foo + def foo = 3 + end + + M.class_eval do + remove_method :foo + define_method :foo, original_foo + end + } + end; + end + + def test_undef_method_error_message_with_zsuper_method + modules = [ + Module.new { private :class }, + Module.new { prepend Module.new { private :class } }, + ] + message = "undefined method 'class' for module '%s'" + modules.each do |mod| + assert_raise_with_message(NameError, message % mod) do + mod.alias_method :xyz, :class + end + end + end end diff --git a/test/ruby/test_allocation.rb b/test/ruby/test_allocation.rb new file mode 100644 index 0000000000..90d7c04f9b --- /dev/null +++ b/test/ruby/test_allocation.rb @@ -0,0 +1,957 @@ +# frozen_string_literal: false +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 + + def check_allocations(checks) + dups = checks.split("\n").reject(&:empty?).tally.select{|_,v| v > 1} + raise "duplicate checks:\n#{dups.keys.join("\n")}" unless dups.empty? + + checks = munge_checks(checks) + + assert_separately([], <<~RUBY) + $allocations = [0, 0] + $counts = {} + failures = [] + + def self.num_allocations + ObjectSpace.count_objects($counts) + arrays = $counts[:T_ARRAY] + hashes = $counts[:T_HASH] + yield + ObjectSpace.count_objects($counts) + arrays -= $counts[:T_ARRAY] + hashes -= $counts[:T_HASH] + $allocations[0] = -arrays + $allocations[1] = -hashes + end + + define_singleton_method(:check_allocations) do |num_arrays, num_hashes, check_code| + instance_eval <<~RB + empty_array = empty_array = [] + empty_hash = empty_hash = {} + array1 = array1 = [1] + r2k_array = r2k_array = [Hash.ruby2_keywords_hash(a: 3)] + r2k_array1 = r2k_array1 = [1, Hash.ruby2_keywords_hash(a: 3)] + r2k_empty_array = r2k_empty_array = [Hash.ruby2_keywords_hash({})] + r2k_empty_array1 = r2k_empty_array1 = [1, Hash.ruby2_keywords_hash({})] + hash1 = hash1 = {a: 2} + nill = nill = nil + block = block = lambda{} + + num_allocations do + \#{check_code} + end + RB + + if num_arrays != $allocations[0] + failures << "Expected \#{num_arrays} array allocations for \#{check_code.inspect}, but \#{$allocations[0]} arrays allocated" + end + if num_hashes != $allocations[1] + failures << "Expected \#{num_hashes} hash allocations for \#{check_code.inspect}, but \#{$allocations[1]} hashes allocated" + end + end + + GC.start + GC.disable + + #{checks} + + assert_empty(failures) + RUBY + end + + class Literal < self + def test_array_literal + check_allocations(<<~RUBY) + check_allocations(1, 0, "[]") + check_allocations(1, 0, "[1]") + check_allocations(1, 0, "[*empty_array]") + check_allocations(1, 0, "[*empty_array, 1, *empty_array]") + check_allocations(1, 0, "[*empty_array, *empty_array]") + check_allocations(1, 0, "[#{'1,'*100000}]") + RUBY + end + + def test_hash_literal + check_allocations(<<~RUBY) + check_allocations(0, 1, "{}") + check_allocations(0, 1, "{a: 1}") + check_allocations(0, 1, "{**empty_hash}") + check_allocations(0, 1, "{**empty_hash, a: 1, **empty_hash}") + check_allocations(0, 1, "{**empty_hash, **empty_hash}") + check_allocations(0, 1, "{#{100000.times.map{|i| "a#{i}: 1"}.join(',')}}") + RUBY + end + end + + class MethodCall < self + def block + '' + end + alias only_block block + + def test_no_parameters + check_allocations(<<~RUBY) + def self.none(#{only_block}); end + + check_allocations(0, 0, "none(#{only_block})") + check_allocations(0, 0, "none(*nil#{block})") + check_allocations(0, 0, "none(*empty_array#{block})") + check_allocations(0, 0, "none(**empty_hash#{block})") + check_allocations(0, 0, "none(*empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "none(*empty_array, *empty_array#{block})") + check_allocations(0, 1, "none(**empty_hash, **empty_hash#{block})") + check_allocations(1, 1, "none(*empty_array, *empty_array, **empty_hash, **empty_hash#{block})") + + check_allocations(0, 0, "none(*r2k_empty_array#{block})") + RUBY + end + + def test_required_parameter + check_allocations(<<~RUBY) + def self.required(x#{block}); end + + check_allocations(0, 0, "required(1#{block})") + check_allocations(0, 0, "required(1, *empty_array#{block})") + check_allocations(0, 0, "required(1, **empty_hash#{block})") + check_allocations(0, 0, "required(1, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "required(*array1#{block})") + check_allocations(0, 1, "required(**hash1#{block})") + + check_allocations(1, 0, "required(*array1, *empty_array#{block})") + check_allocations(0, 1, "required(**hash1, **empty_hash#{block})") + check_allocations(1, 0, "required(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "required(*r2k_empty_array1#{block})") + check_allocations(0, 1, "required(*r2k_array#{block})") + + check_allocations(0, 1, "required(*empty_array, **hash1, **empty_hash#{block})") + RUBY + end + + def test_optional_parameter + check_allocations(<<~RUBY) + def self.optional(x=nil#{block}); end + + check_allocations(0, 0, "optional(1#{block})") + check_allocations(0, 0, "optional(1, *empty_array#{block})") + check_allocations(0, 0, "optional(1, **empty_hash#{block})") + check_allocations(0, 0, "optional(1, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "optional(*array1#{block})") + check_allocations(0, 1, "optional(**hash1#{block})") + + check_allocations(1, 0, "optional(*array1, *empty_array#{block})") + check_allocations(0, 1, "optional(**hash1, **empty_hash#{block})") + check_allocations(1, 0, "optional(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "optional(*r2k_empty_array#{block})") + check_allocations(0, 0, "optional(*r2k_empty_array1#{block})") + check_allocations(0, 1, "optional(*r2k_array#{block})") + + check_allocations(0, 0, "optional(*empty_array#{block})") + check_allocations(0, 0, "optional(*nil#{block})") + check_allocations(0, 0, "optional(#{only_block})") + check_allocations(0, 1, "optional(*empty_array, **hash1, **empty_hash#{block})") + RUBY + end + + def test_positional_splat_parameter + check_allocations(<<~RUBY) + def self.splat(*x#{block}); end + + check_allocations(1, 0, "splat(1#{block})") + check_allocations(1, 0, "splat(1, *empty_array#{block})") + check_allocations(1, 0, "splat(1, **empty_hash#{block})") + check_allocations(1, 0, "splat(1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "splat(*array1#{block})") + check_allocations(1, 0, "splat(*array1, *empty_array#{block})") + check_allocations(1, 0, "splat(*array1, **empty_hash#{block})") + check_allocations(1, 0, "splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "splat(1, *array1#{block})") + check_allocations(1, 0, "splat(1, *array1, *empty_array#{block})") + check_allocations(1, 0, "splat(1, *array1, **empty_hash#{block})") + check_allocations(1, 0, "splat(1, *array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "splat(*nil#{block})") + check_allocations(1, 0, "splat(#{only_block})") + check_allocations(1, 1, "splat(**hash1#{block})") + + check_allocations(1, 1, "splat(**hash1, **empty_hash#{block})") + check_allocations(1, 1, "splat(*empty_array, **hash1, **empty_hash#{block})") + + check_allocations(1, 0, "splat(*r2k_empty_array#{block})") + check_allocations(1, 0, "splat(*r2k_empty_array1#{block})") + check_allocations(1, 1, "splat(*r2k_array#{block})") + check_allocations(1, 1, "splat(*r2k_array1#{block})") + RUBY + end + + def test_required_and_positional_splat_parameters + check_allocations(<<~RUBY) + def self.req_splat(x, *y#{block}); end + + check_allocations(1, 0, "req_splat(1#{block})") + check_allocations(1, 0, "req_splat(1, *nil#{block})") + check_allocations(1, 0, "req_splat(1, *empty_array#{block})") + check_allocations(1, 0, "req_splat(1, **empty_hash#{block})") + check_allocations(1, 0, "req_splat(1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "req_splat(*array1#{block})") + check_allocations(1, 0, "req_splat(*array1, *empty_array#{block})") + check_allocations(1, 0, "req_splat(*array1, **empty_hash#{block})") + check_allocations(1, 0, "req_splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "req_splat(1, *array1#{block})") + check_allocations(1, 0, "req_splat(1, *array1, *empty_array#{block})") + check_allocations(1, 0, "req_splat(1, *array1, **empty_hash#{block})") + check_allocations(1, 0, "req_splat(1, *array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "req_splat(**hash1#{block})") + + check_allocations(1, 1, "req_splat(**hash1, **empty_hash#{block})") + check_allocations(1, 1, "req_splat(*empty_array, **hash1, **empty_hash#{block})") + + check_allocations(1, 0, "req_splat(*r2k_empty_array1#{block})") + check_allocations(1, 1, "req_splat(*r2k_array#{block})") + check_allocations(1, 1, "req_splat(*r2k_array1#{block})") + RUBY + end + + def test_positional_splat_and_post_parameters + check_allocations(<<~RUBY) + def self.splat_post(*x, y#{block}); end + + check_allocations(1, 0, "splat_post(1#{block})") + check_allocations(1, 0, "splat_post(1, *nil#{block})") + check_allocations(1, 0, "splat_post(1, *empty_array#{block})") + check_allocations(1, 0, "splat_post(1, **empty_hash#{block})") + check_allocations(1, 0, "splat_post(1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "splat_post(*array1#{block})") + check_allocations(1, 0, "splat_post(*array1, *empty_array#{block})") + check_allocations(1, 0, "splat_post(*array1, **empty_hash#{block})") + check_allocations(1, 0, "splat_post(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "splat_post(1, *array1#{block})") + check_allocations(1, 0, "splat_post(1, *array1, *empty_array#{block})") + check_allocations(1, 0, "splat_post(1, *array1, **empty_hash#{block})") + check_allocations(1, 0, "splat_post(1, *array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "splat_post(**hash1#{block})") + + check_allocations(1, 1, "splat_post(**hash1, **empty_hash#{block})") + check_allocations(1, 1, "splat_post(*empty_array, **hash1, **empty_hash#{block})") + + check_allocations(1, 0, "splat_post(*r2k_empty_array1#{block})") + check_allocations(1, 1, "splat_post(*r2k_array#{block})") + check_allocations(1, 1, "splat_post(*r2k_array1#{block})") + RUBY + end + + def test_keyword_parameter + check_allocations(<<~RUBY) + def self.keyword(a: nil#{block}); end + + check_allocations(0, 0, "keyword(a: 2#{block})") + check_allocations(0, 0, "keyword(*empty_array, a: 2#{block})") + check_allocations(0, 1, "keyword(a:2, **empty_hash#{block})") + check_allocations(0, 1, "keyword(**empty_hash, a: 2#{block})") + + check_allocations(0, 0, "keyword(**nil#{block})") + check_allocations(0, 0, "keyword(**empty_hash#{block})") + check_allocations(0, 0, "keyword(**hash1#{block})") + check_allocations(0, 0, "keyword(*empty_array, **hash1#{block})") + check_allocations(0, 1, "keyword(**hash1, **empty_hash#{block})") + check_allocations(0, 1, "keyword(**empty_hash, **hash1#{block})") + + check_allocations(0, 0, "keyword(*nil#{block})") + check_allocations(0, 0, "keyword(*empty_array#{block})") + check_allocations(1, 0, "keyword(*empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "keyword(*r2k_empty_array#{block})") + check_allocations(0, 0, "keyword(*r2k_array#{block})") + + check_allocations(0, 1, "keyword(*empty_array, a: 2, **empty_hash#{block})") + check_allocations(0, 1, "keyword(*empty_array, **hash1, **empty_hash#{block})") + RUBY + end + + def test_keyword_splat_parameter + check_allocations(<<~RUBY) + def self.keyword_splat(**kw#{block}); end + + check_allocations(0, 1, "keyword_splat(a: 2#{block})") + check_allocations(0, 1, "keyword_splat(*empty_array, a: 2#{block})") + check_allocations(0, 1, "keyword_splat(a:2, **empty_hash#{block})") + check_allocations(0, 1, "keyword_splat(**empty_hash, a: 2#{block})") + + check_allocations(0, 1, "keyword_splat(**nil#{block})") + check_allocations(0, 1, "keyword_splat(**empty_hash#{block})") + check_allocations(0, 1, "keyword_splat(**hash1#{block})") + check_allocations(0, 1, "keyword_splat(*empty_array, **hash1#{block})") + check_allocations(0, 1, "keyword_splat(**hash1, **empty_hash#{block})") + check_allocations(0, 1, "keyword_splat(**empty_hash, **hash1#{block})") + + check_allocations(0, 1, "keyword_splat(*nil#{block})") + check_allocations(0, 1, "keyword_splat(*empty_array#{block})") + check_allocations(1, 1, "keyword_splat(*empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 1, "keyword_splat(*r2k_empty_array#{block})") + check_allocations(0, 1, "keyword_splat(*r2k_array#{block})") + + check_allocations(0, 1, "keyword_splat(*empty_array, a: 2, **empty_hash#{block})") + check_allocations(0, 1, "keyword_splat(*empty_array, **hash1, **empty_hash#{block})") + RUBY + end + + def test_keyword_and_keyword_splat_parameter + check_allocations(<<~RUBY) + def self.keyword_and_keyword_splat(a: 1, **kw#{block}); end + + check_allocations(0, 1, "keyword_and_keyword_splat(a: 2#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(*empty_array, a: 2#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(a:2, **empty_hash#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(**empty_hash, a: 2#{block})") + + check_allocations(0, 1, "keyword_and_keyword_splat(**nil#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(**empty_hash#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(**hash1#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(*empty_array, **hash1#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(**hash1, **empty_hash#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(**empty_hash, **hash1#{block})") + + check_allocations(0, 1, "keyword_and_keyword_splat(*nil#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(*empty_array#{block})") + check_allocations(1, 1, "keyword_and_keyword_splat(*empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 1, "keyword_and_keyword_splat(*r2k_empty_array#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(*r2k_array#{block})") + + check_allocations(0, 1, "keyword_and_keyword_splat(*empty_array, a: 2, **empty_hash#{block})") + check_allocations(0, 1, "keyword_and_keyword_splat(*empty_array, **hash1, **empty_hash#{block})") + RUBY + end + + def test_required_positional_and_keyword_parameter + check_allocations(<<~RUBY) + def self.required_and_keyword(b, a: nil#{block}); end + + check_allocations(0, 0, "required_and_keyword(1, a: 2#{block})") + check_allocations(0, 0, "required_and_keyword(1, *empty_array, a: 2#{block})") + check_allocations(0, 1, "required_and_keyword(1, a:2, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword(1, **empty_hash, a: 2#{block})") + + check_allocations(0, 0, "required_and_keyword(1, **nil#{block})") + check_allocations(0, 0, "required_and_keyword(1, **empty_hash#{block})") + check_allocations(0, 0, "required_and_keyword(1, **hash1#{block})") + check_allocations(0, 0, "required_and_keyword(1, *empty_array, **hash1#{block})") + check_allocations(0, 1, "required_and_keyword(1, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword(1, **empty_hash, **hash1#{block})") + + check_allocations(0, 0, "required_and_keyword(1, *nil#{block})") + check_allocations(0, 0, "required_and_keyword(1, *empty_array#{block})") + check_allocations(1, 0, "required_and_keyword(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "required_and_keyword(*array1, a: 2#{block})") + + check_allocations(0, 0, "required_and_keyword(*array1, **nill#{block})") + check_allocations(0, 0, "required_and_keyword(*array1, **empty_hash#{block})") + check_allocations(0, 0, "required_and_keyword(*array1, **hash1#{block})") + check_allocations(1, 0, "required_and_keyword(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "required_and_keyword(*array1, *empty_array#{block})") + check_allocations(1, 0, "required_and_keyword(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "required_and_keyword(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "required_and_keyword(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 0, "required_and_keyword(*r2k_empty_array1#{block})") + check_allocations(0, 0, "required_and_keyword(*r2k_array1#{block})") + + check_allocations(0, 1, "required_and_keyword(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword(*array1, **empty_hash, a: 2#{block})") + check_allocations(0, 1, "required_and_keyword(*array1, **hash1, **empty_hash#{block})") + check_allocations(0, 0, "required_and_keyword(*array1, **nil#{block})") + RUBY + end + + def test_positional_splat_and_keyword_parameter + check_allocations(<<~RUBY) + def self.splat_and_keyword(*b, a: nil#{block}); end + + check_allocations(1, 0, "splat_and_keyword(1, a: 2#{block})") + check_allocations(1, 0, "splat_and_keyword(1, *empty_array, a: 2#{block})") + check_allocations(1, 1, "splat_and_keyword(1, a:2, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword(1, **empty_hash, a: 2#{block})") + + check_allocations(1, 0, "splat_and_keyword(1, **nil#{block})") + check_allocations(1, 0, "splat_and_keyword(1, **empty_hash#{block})") + check_allocations(1, 0, "splat_and_keyword(1, **hash1#{block})") + check_allocations(1, 0, "splat_and_keyword(1, *empty_array, **hash1#{block})") + check_allocations(1, 1, "splat_and_keyword(1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword(1, **empty_hash, **hash1#{block})") + + check_allocations(1, 0, "splat_and_keyword(1, *nil#{block})") + check_allocations(1, 0, "splat_and_keyword(1, *empty_array#{block})") + check_allocations(1, 0, "splat_and_keyword(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(1, 0, "splat_and_keyword(*array1, a: 2#{block})") + + check_allocations(1, 0, "splat_and_keyword(*array1, **nill#{block})") + check_allocations(1, 0, "splat_and_keyword(*array1, **empty_hash#{block})") + check_allocations(1, 0, "splat_and_keyword(*array1, **hash1#{block})") + check_allocations(1, 0, "splat_and_keyword(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "splat_and_keyword(*array1, *empty_array#{block})") + check_allocations(1, 0, "splat_and_keyword(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "splat_and_keyword(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(1, 1, "splat_and_keyword(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword(*array1, **empty_hash, a: 2#{block})") + check_allocations(1, 1, "splat_and_keyword(*array1, **hash1, **empty_hash#{block})") + check_allocations(1, 0, "splat_and_keyword(*array1, **nil#{block})") + + check_allocations(1, 0, "splat_and_keyword(*r2k_empty_array#{block})") + check_allocations(1, 0, "splat_and_keyword(*r2k_array#{block})") + check_allocations(1, 0, "splat_and_keyword(*r2k_empty_array1#{block})") + check_allocations(1, 0, "splat_and_keyword(*r2k_array1#{block})") + RUBY + end + + def test_required_and_keyword_splat_parameter + check_allocations(<<~RUBY) + def self.required_and_keyword_splat(b, **kw#{block}); end + + check_allocations(0, 1, "required_and_keyword_splat(1, a: 2#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, *empty_array, a: 2#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, a:2, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, **empty_hash, a: 2#{block})") + + check_allocations(0, 1, "required_and_keyword_splat(1, **nil#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, **hash1#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, *empty_array, **hash1#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, **empty_hash, **hash1#{block})") + + check_allocations(0, 1, "required_and_keyword_splat(1, *nil#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, *empty_array#{block})") + check_allocations(1, 1, "required_and_keyword_splat(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 1, "required_and_keyword_splat(*array1, a: 2#{block})") + + check_allocations(0, 1, "required_and_keyword_splat(*array1, **nill#{block})") + check_allocations(0, 1, "required_and_keyword_splat(*array1, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword_splat(*array1, **hash1#{block})") + check_allocations(1, 1, "required_and_keyword_splat(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 1, "required_and_keyword_splat(*array1, *empty_array#{block})") + check_allocations(1, 1, "required_and_keyword_splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "required_and_keyword_splat(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "required_and_keyword_splat(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 1, "required_and_keyword_splat(*r2k_empty_array1#{block})") + check_allocations(0, 1, "required_and_keyword_splat(*r2k_array1#{block})") + + check_allocations(0, 1, "required_and_keyword_splat(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword_splat(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword_splat(*array1, **empty_hash, a: 2#{block})") + check_allocations(0, 1, "required_and_keyword_splat(*array1, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "required_and_keyword_splat(*array1, **nil#{block})") + RUBY + end + + def test_positional_splat_and_keyword_splat_parameter + check_allocations(<<~RUBY) + def self.splat_and_keyword_splat(*b, **kw#{block}); end + + check_allocations(1, 1, "splat_and_keyword_splat(1, a: 2#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, *empty_array, a: 2#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, a:2, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, **empty_hash, a: 2#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(1, **nil#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, **hash1#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, *empty_array, **hash1#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, **empty_hash, **hash1#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(1, *nil#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, *empty_array#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(*array1, a: 2#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(*array1, **nill#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, **hash1#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(*array1, *empty_array#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, **empty_hash, a: 2#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*array1, **nil#{block})") + + check_allocations(1, 1, "splat_and_keyword_splat(*r2k_empty_array#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*r2k_array#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*r2k_empty_array1#{block})") + check_allocations(1, 1, "splat_and_keyword_splat(*r2k_array1#{block})") + 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) + def self.anon_splat_and_anon_keyword_splat(*, **#{block}); end + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, a: 2#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array, a: 2#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, a:2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, **empty_hash, a: 2#{block})") + + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, **nil#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, **empty_hash#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, **hash1#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array, **hash1#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, **empty_hash, **hash1#{block})") + + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, a: 2#{block})") + + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*nil, **nill#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **nill#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **empty_hash#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **hash1#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(*array1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(#{only_block})") + check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(a: 2#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(**empty_hash#{block})") + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(*array1, **empty_hash, a: 2#{block})") + check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(*array1, **hash1, **empty_hash#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **nil#{block})") + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_empty_array#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_array#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_empty_array1#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_array1#{block})") + RUBY + end + + def test_nested_anonymous_splat_and_anonymous_keyword_splat_parameters + only_block = block.empty? ? block : block[2..] + check_allocations(<<~RUBY) + def self.t(*, **#{block}); end + def self.anon_splat_and_anon_keyword_splat(*, **#{block}); t(*, **) end + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, a: 2#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array, a: 2#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, a:2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, **empty_hash, a: 2#{block})") + + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, **nil#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, **empty_hash#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, **hash1#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array, **hash1#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, **empty_hash, **hash1#{block})") + + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, a: 2#{block})") + + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*nil, **nill#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **nill#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **empty_hash#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **hash1#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(*array1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(#{only_block})") + check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(a: 2#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(**empty_hash#{block})") + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(*array1, **empty_hash, a: 2#{block})") + check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(*array1, **hash1, **empty_hash#{block})") + check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(*array1, **nil#{block})") + + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_empty_array#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_array#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_empty_array1#{block})") + check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*r2k_array1#{block})") + RUBY + end + + def test_argument_forwarding + check_allocations(<<~RUBY) + def self.argument_forwarding(...); end + + check_allocations(0, 0, "argument_forwarding(1, a: 2#{block})") + check_allocations(0, 0, "argument_forwarding(1, *empty_array, a: 2#{block})") + check_allocations(0, 1, "argument_forwarding(1, a:2, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(1, **empty_hash, a: 2#{block})") + + check_allocations(0, 0, "argument_forwarding(1, **nil#{block})") + check_allocations(0, 0, "argument_forwarding(1, **empty_hash#{block})") + check_allocations(0, 0, "argument_forwarding(1, **hash1#{block})") + check_allocations(0, 0, "argument_forwarding(1, *empty_array, **hash1#{block})") + check_allocations(0, 1, "argument_forwarding(1, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(1, **empty_hash, **hash1#{block})") + + check_allocations(0, 0, "argument_forwarding(1, *empty_array#{block})") + check_allocations(1, 0, "argument_forwarding(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "argument_forwarding(*array1, a: 2#{block})") + + check_allocations(0, 0, "argument_forwarding(**nill#{block})") + check_allocations(0, 0, "argument_forwarding(*nil, **nill#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **nill#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **empty_hash#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **hash1#{block})") + check_allocations(1, 0, "argument_forwarding(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "argument_forwarding(*array1, *empty_array#{block})") + check_allocations(1, 0, "argument_forwarding(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "argument_forwarding(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "argument_forwarding(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 1, "argument_forwarding(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(*array1, **empty_hash, a: 2#{block})") + check_allocations(0, 1, "argument_forwarding(*array1, **hash1, **empty_hash#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **nil#{block})") + + check_allocations(0, 0, "argument_forwarding(*r2k_empty_array#{block})") + check_allocations(0, 0, "argument_forwarding(*r2k_array#{block})") + check_allocations(0, 0, "argument_forwarding(*r2k_empty_array1#{block})") + check_allocations(0, 0, "argument_forwarding(*r2k_array1#{block})") + RUBY + end + + def test_nested_argument_forwarding + check_allocations(<<~RUBY) + def self.t(...) end + def self.argument_forwarding(...); t(...) end + + check_allocations(0, 0, "argument_forwarding(1, a: 2#{block})") + check_allocations(0, 0, "argument_forwarding(1, *empty_array, a: 2#{block})") + check_allocations(0, 1, "argument_forwarding(1, a:2, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(1, **empty_hash, a: 2#{block})") + + check_allocations(0, 0, "argument_forwarding(1, **nil#{block})") + check_allocations(0, 0, "argument_forwarding(1, **empty_hash#{block})") + check_allocations(0, 0, "argument_forwarding(1, **hash1#{block})") + check_allocations(0, 0, "argument_forwarding(1, *empty_array, **hash1#{block})") + check_allocations(0, 1, "argument_forwarding(1, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(1, **empty_hash, **hash1#{block})") + + check_allocations(0, 0, "argument_forwarding(1, *empty_array#{block})") + check_allocations(1, 0, "argument_forwarding(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(0, 0, "argument_forwarding(*array1, a: 2#{block})") + + check_allocations(0, 0, "argument_forwarding(**nill#{block})") + check_allocations(0, 0, "argument_forwarding(*nil, **nill#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **nill#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **empty_hash#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **hash1#{block})") + check_allocations(1, 0, "argument_forwarding(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "argument_forwarding(*array1, *empty_array#{block})") + check_allocations(1, 0, "argument_forwarding(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "argument_forwarding(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "argument_forwarding(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 1, "argument_forwarding(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(0, 1, "argument_forwarding(*array1, **empty_hash, a: 2#{block})") + check_allocations(0, 1, "argument_forwarding(*array1, **hash1, **empty_hash#{block})") + check_allocations(0, 0, "argument_forwarding(*array1, **nil#{block})") + + check_allocations(0, 0, "argument_forwarding(*r2k_empty_array#{block})") + check_allocations(0, 0, "argument_forwarding(*r2k_array#{block})") + check_allocations(0, 0, "argument_forwarding(*r2k_empty_array1#{block})") + check_allocations(0, 0, "argument_forwarding(*r2k_array1#{block})") + RUBY + end + + def test_ruby2_keywords + check_allocations(<<~RUBY) + def self.r2k(*a#{block}); end + singleton_class.send(:ruby2_keywords, :r2k) + + check_allocations(1, 1, "r2k(1, a: 2#{block})") + check_allocations(1, 1, "r2k(1, *empty_array, a: 2#{block})") + check_allocations(1, 1, "r2k(1, a:2, **empty_hash#{block})") + check_allocations(1, 1, "r2k(1, **empty_hash, a: 2#{block})") + + check_allocations(1, 0, "r2k(1, **nil#{block})") + check_allocations(1, 0, "r2k(1, **empty_hash#{block})") + check_allocations(1, 1, "r2k(1, **hash1#{block})") + check_allocations(1, 1, "r2k(1, *empty_array, **hash1#{block})") + check_allocations(1, 1, "r2k(1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "r2k(1, **empty_hash, **hash1#{block})") + + check_allocations(1, 0, "r2k(1, *empty_array#{block})") + check_allocations(1, 0, "r2k(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "r2k(*array1, a: 2#{block})") + + check_allocations(1, 0, "r2k(**nill#{block})") + check_allocations(1, 0, "r2k(*nil, **nill#{block})") + check_allocations(1, 0, "r2k(*array1, **nill#{block})") + check_allocations(1, 0, "r2k(*array1, **empty_hash#{block})") + check_allocations(1, 1, "r2k(*array1, **hash1#{block})") + check_allocations(1, 1, "r2k(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "r2k(*array1, *empty_array#{block})") + check_allocations(1, 0, "r2k(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "r2k(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "r2k(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(1, 1, "r2k(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "r2k(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "r2k(*array1, **empty_hash, a: 2#{block})") + check_allocations(1, 1, "r2k(*array1, **hash1, **empty_hash#{block})") + check_allocations(1, 0, "r2k(*array1, **nil#{block})") + + check_allocations(1, 0, "r2k(*r2k_empty_array#{block})") + unless defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled? + # YJIT may or may not allocate depending on arch? + check_allocations(1, 1, "r2k(*r2k_array#{block})") + check_allocations(1, 0, "r2k(*r2k_empty_array1#{block})") + check_allocations(1, 1, "r2k(*r2k_array1#{block})") + end + RUBY + end + + 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 + 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 - 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 + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: 1.0#{block})") # FLOAT + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: 1.0r#{block})") # RATIONAL + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: 1.0i#{block})") # IMAGINARY + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: 'a'#{block})") # STR + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: :b#{block})") # SYM + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: /a/#{block})") # REGX + check_allocations(0, 1, "keyword(*empty_array, a: self#{block})") # SELF + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: nil#{block})") # NIL + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: true#{block})") # TRUE + check_allocations(0, 1, "keyword(*empty_array, a: empty_array, b: false#{block})") # FALSE + 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 + + class WithBlock < self + def block + ', &block' + end + def only_block + '&block' + end + end + end + + class ProcCall < MethodCall + def munge_checks(checks) + return checks if @no_munge + sub = rep = nil + checks.split("\n").map do |line| + case line + when "singleton_class.send(:ruby2_keywords, :r2k)" + "r2k.ruby2_keywords" + when /\Adef self.([a-z0-9_]+)\((.*)\);(.*)end\z/ + sub = $1 + '(' + rep = $1 + '.(' + "#{$1} = #{$1} = proc{ |#{$2}| #{$3} }" + when /check_allocations/ + line.gsub(sub, rep) + else + line + end + end.join("\n") + end + + # Generic argument forwarding not supported in proc definitions + undef_method :test_argument_forwarding + undef_method :test_nested_argument_forwarding + + # Proc anonymous arguments cannot be used directly + undef_method :test_nested_anonymous_splat_and_anonymous_keyword_splat_parameters + + def test_no_array_allocation_with_splat_and_nonstatic_keywords + @no_munge = true + + 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 - 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 + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1.0#{block})") # FLOAT + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1.0r#{block})") # RATIONAL + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1.0i#{block})") # IMAGINARY + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 'a'#{block})") # STR + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: :b#{block})") # SYM + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: /a/#{block})") # REGX + check_allocations(0, 1, "keyword.(*empty_array, a: self#{block})") # SELF + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: nil#{block})") # NIL + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: true#{block})") # TRUE + check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: false#{block})") # FALSE + 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 + + class WithBlock < self + def block + ', &block' + end + def only_block + '&block' + end + end + end +end diff --git a/test/ruby/test_argf.rb b/test/ruby/test_argf.rb index 12f7d6485a..55a06296aa 100644 --- a/test/ruby/test_argf.rb +++ b/test/ruby/test_argf.rb @@ -9,39 +9,23 @@ class TestArgf < Test::Unit::TestCase def setup @tmpdir = Dir.mktmpdir @tmp_count = 0 - @t1 = make_tempfile0("argf-foo") - @t1.binmode - @t1.puts "1" - @t1.puts "2" - @t1.close - @t2 = make_tempfile0("argf-bar") - @t2.binmode - @t2.puts "3" - @t2.puts "4" - @t2.close - @t3 = make_tempfile0("argf-baz") - @t3.binmode - @t3.puts "5" - @t3.puts "6" - @t3.close + @t1 = make_tempfile("argf-foo", %w"1 2", binmode: true) + @t2 = make_tempfile("argf-bar", %w"3 4", binmode: true) + @t3 = make_tempfile("argf-baz", %w"5 6", binmode: true) end def teardown FileUtils.rmtree(@tmpdir) end - def make_tempfile0(basename) + def make_tempfile(basename = "argf-qux", data = %w[foo bar baz], binmode: false) @tmp_count += 1 - open("#{@tmpdir}/#{basename}-#{@tmp_count}", "w") - end - - def make_tempfile(basename = "argf-qux") - t = make_tempfile0(basename) - t.puts "foo" - t.puts "bar" - t.puts "baz" - t.close - t + path = "#{@tmpdir}/#{basename}-#{@tmp_count}" + File.open(path, "w") do |f| + f.binmode if binmode + f.puts(*data) + f + end end def ruby(*args, external_encoding: Encoding::UTF_8) @@ -571,15 +555,11 @@ class TestArgf < Test::Unit::TestCase end end - t1 = open("#{@tmpdir}/argf-hoge", "w") - t1.binmode - t1.puts "foo" - t1.close - t2 = open("#{@tmpdir}/argf-moge", "w") - t2.binmode - t2.puts "bar" - t2.close - ruby('-e', 'STDERR.reopen(STDOUT); ARGF.gets; ARGF.skip; p ARGF.eof?', t1.path, t2.path) do |f| + t1 = "#{@tmpdir}/argf-hoge" + t2 = "#{@tmpdir}/argf-moge" + File.binwrite(t1, "foo\n") + File.binwrite(t2, "bar\n") + ruby('-e', 'STDERR.reopen(STDOUT); ARGF.gets; ARGF.skip; p ARGF.eof?', t1, t2) do |f| assert_equal(%w(false), f.read.split(/\n/)) end end @@ -593,7 +573,7 @@ class TestArgf < Test::Unit::TestCase def test_read2 ruby('-e', "#{<<~"{#"}\n#{<<~'};'}", @t1.path, @t2.path, @t3.path) do |f| {# - s = "" + s = +"" ARGF.read(8, s) p s }; @@ -604,7 +584,7 @@ class TestArgf < Test::Unit::TestCase def test_read2_with_not_empty_buffer ruby('-e', "#{<<~"{#"}\n#{<<~'};'}", @t1.path, @t2.path, @t3.path) do |f| {# - s = "0123456789" + s = +"0123456789" ARGF.read(8, s) p s }; @@ -617,7 +597,7 @@ class TestArgf < Test::Unit::TestCase {# nil while ARGF.gets p ARGF.read - p ARGF.read(0, "") + p ARGF.read(0, +"") }; assert_equal("nil\n\"\"\n", f.read) end @@ -626,13 +606,13 @@ class TestArgf < Test::Unit::TestCase def test_readpartial ruby('-e', "#{<<~"{#"}\n#{<<~'};'}", @t1.path, @t2.path, @t3.path) do |f| {# - s = "" + s = +"" begin loop do s << ARGF.readpartial(1) - t = ""; ARGF.readpartial(1, t); s << t + t = +""; ARGF.readpartial(1, t); s << t # not empty buffer - u = "abcdef"; ARGF.readpartial(1, u); s << u + u = +"abcdef"; ARGF.readpartial(1, u); s << u end rescue EOFError puts s @@ -645,11 +625,11 @@ class TestArgf < Test::Unit::TestCase def test_readpartial2 ruby('-e', "#{<<~"{#"}\n#{<<~'};'}") do |f| {# - s = "" + s = +"" begin loop do s << ARGF.readpartial(1) - t = ""; ARGF.readpartial(1, t); s << t + t = +""; ARGF.readpartial(1, t); s << t end rescue EOFError $stdout.binmode @@ -680,7 +660,7 @@ class TestArgf < Test::Unit::TestCase def test_getc ruby('-e', "#{<<~"{#"}\n#{<<~'};'}", @t1.path, @t2.path, @t3.path) do |f| {# - s = "" + s = +"" while c = ARGF.getc s << c end @@ -706,7 +686,7 @@ class TestArgf < Test::Unit::TestCase def test_readchar ruby('-e', "#{<<~"{#"}\n#{<<~'};'}", @t1.path, @t2.path, @t3.path) do |f| {# - s = "" + s = +"" begin while c = ARGF.readchar s << c @@ -784,7 +764,7 @@ class TestArgf < Test::Unit::TestCase def test_each_char ruby('-e', "#{<<~"{#"}\n#{<<~'};'}", @t1.path, @t2.path, @t3.path) do |f| {# - s = "" + s = +"" ARGF.each_char {|c| s << c } puts s }; @@ -854,7 +834,7 @@ class TestArgf < Test::Unit::TestCase def test_binmode bug5268 = '[ruby-core:39234]' - open(@t3.path, "wb") {|f| f.write "5\r\n6\r\n"} + File.binwrite(@t3.path, "5\r\n6\r\n") ruby('-e', "ARGF.binmode; STDOUT.binmode; puts ARGF.read", @t1.path, @t2.path, @t3.path) do |f| f.binmode assert_equal("1\n2\n3\n4\n5\r\n6\r\n", f.read, bug5268) @@ -863,7 +843,7 @@ class TestArgf < Test::Unit::TestCase def test_textmode bug5268 = '[ruby-core:39234]' - open(@t3.path, "wb") {|f| f.write "5\r\n6\r\n"} + File.binwrite(@t3.path, "5\r\n6\r\n") ruby('-e', "STDOUT.binmode; puts ARGF.read", @t1.path, @t2.path, @t3.path) do |f| f.binmode assert_equal("1\n2\n3\n4\n5\n6\n", f.read, bug5268) @@ -1073,7 +1053,7 @@ class TestArgf < Test::Unit::TestCase ruby('-e', "#{<<~"{#"}\n#{<<~'};'}") do |f| {# $stdout.sync = true - :wait_readable == ARGF.read_nonblock(1, "", exception: false) or + :wait_readable == ARGF.read_nonblock(1, +"", exception: false) or abort "did not return :wait_readable" begin @@ -1086,7 +1066,7 @@ class TestArgf < Test::Unit::TestCase IO.select([ARGF]) == [[ARGF], [], []] or abort 'did not awaken for readability (before byte)' - buf = '' + buf = +'' buf.object_id == ARGF.read_nonblock(1, buf).object_id or abort "read destination buffer failed" print buf @@ -1140,4 +1120,34 @@ class TestArgf < Test::Unit::TestCase argf.close end end + + def test_putc + t = make_tempfile("argf-#{__method__}", 'bar') + ruby('-pi-', '-e', "print ARGF.putc('x')", t.path) do |f| + end + assert_equal("xxbar\n", File.read(t.path)) + end + + def test_puts + t = make_tempfile("argf-#{__method__}", 'bar') + err = "#{@tmpdir}/errout" + ruby('-pi-', '-W2', '-e', "print ARGF.puts('foo')", t.path, {err: err}) do |f| + end + assert_equal("foo\nbar\n", File.read(t.path)) + assert_empty File.read(err) + end + + def test_print + t = make_tempfile("argf-#{__method__}", 'bar') + ruby('-pi-', '-e', "print ARGF.print('foo')", t.path) do |f| + end + assert_equal("foobar\n", File.read(t.path)) + end + + def test_printf + t = make_tempfile("argf-#{__method__}", 'bar') + ruby('-pi-', '-e', "print ARGF.printf('%s', 'foo')", t.path) do |f| + end + assert_equal("foobar\n", File.read(t.path)) + end end diff --git a/test/ruby/test_arity.rb b/test/ruby/test_arity.rb index d26338e0aa..bd26d5f0f5 100644 --- a/test/ruby/test_arity.rb +++ b/test/ruby/test_arity.rb @@ -65,6 +65,5 @@ class TestArity < Test::Unit::TestCase assert_arity(%w[1 2]) { "".sub!(//) } assert_arity(%w[0 1..2]) { "".sub!{} } assert_arity(%w[0 1+]) { exec } - assert_arity(%w[0 1+]) { Struct.new } end end diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index f58f8a2778..04e15b6d87 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -529,14 +529,19 @@ class TestArray < Test::Unit::TestCase end def test_assoc + def (a4 = Object.new).to_ary + %w( pork porcine ) + end + a1 = @cls[*%w( cat feline )] a2 = @cls[*%w( dog canine )] a3 = @cls[*%w( mule asinine )] - a = @cls[ a1, a2, a3 ] + a = @cls[ a1, a2, a3, a4 ] assert_equal(a1, a.assoc('cat')) assert_equal(a3, a.assoc('mule')) + assert_equal(%w( pork porcine ), a.assoc("pork")) assert_equal(nil, a.assoc('asinine')) assert_equal(nil, a.assoc('wombat')) assert_equal(nil, a.assoc(1..2)) @@ -1109,6 +1114,33 @@ class TestArray < Test::Unit::TestCase assert_not_include(a, [1,2]) end + def test_monkey_patch_include? + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30) + begin; + $-w = false + class Array + alias :old_include? :include? + def include? x + return true if x == :always + old_include?(x) + end + end + def test + a, c, always = :a, :c, :always + [ + [:a, :b].include?(a), + [:a, :b].include?(c), + [:a, :b].include?(always), + ] + end + v = test + class Array + alias :include? :old_include? + end + assert_equal [true, false, true], v + end; + end + def test_intersect? a = @cls[ 1, 2, 3] assert_send([a, :intersect?, [3]]) @@ -1210,6 +1242,17 @@ class TestArray < Test::Unit::TestCase assert_equal(@cls[], a) end + def test_pack_format_mutation + ary = [Object.new] + fmt = "c" * 0x20000 + class << ary[0]; self end.send(:define_method, :to_int) { + fmt.replace "" + 1 + } + e = assert_raise(RuntimeError) { ary.pack(fmt) } + assert_equal "format string modified", e.message + end + def test_pack a = @cls[*%w( cat wombat x yy)] assert_equal("catwomx yy ", a.pack("A3A3A3A3")) @@ -1266,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 @@ -1318,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)) @@ -1329,13 +1369,17 @@ class TestArray < Test::Unit::TestCase end def test_rassoc + def (a4 = Object.new).to_ary + %w( pork porcine ) + end a1 = @cls[*%w( cat feline )] a2 = @cls[*%w( dog canine )] a3 = @cls[*%w( mule asinine )] - a = @cls[ a1, a2, a3 ] + a = @cls[ a1, a2, a3, a4 ] assert_equal(a1, a.rassoc('feline')) assert_equal(a3, a.rassoc('asinine')) + assert_equal(%w( pork porcine ), a.rassoc("porcine")) assert_equal(nil, a.rassoc('dog')) assert_equal(nil, a.rassoc('mule')) assert_equal(nil, a.rassoc(1..2)) @@ -1693,6 +1737,15 @@ class TestArray < Test::Unit::TestCase assert_equal([100], a.slice(-1, 1_000_000_000)) end + def test_slice_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress { assert_equal([1, 2, 3, 4, 5], (0..10).to_a[1, 5]) } + EnvUtil.under_gc_compact_stress do + a = [0, 1, 2, 3, 4, 5] + assert_equal([2, 1, 0], a.slice((2..).step(-1))) + end + end + def test_slice! a = @cls[1, 2, 3, 4, 5] assert_equal(3, a.slice!(2)) @@ -2660,6 +2713,18 @@ class TestArray < Test::Unit::TestCase assert_equal(2, [0, 1].fetch(2, 2)) end + def test_fetch_values + ary = @cls[1, 2, 3] + assert_equal([], ary.fetch_values()) + assert_equal([1], ary.fetch_values(0)) + assert_equal([3, 1, 3], ary.fetch_values(2, 0, -1)) + assert_raise(TypeError) {ary.fetch_values("")} + assert_raise(IndexError) {ary.fetch_values(10)} + assert_raise(IndexError) {ary.fetch_values(-20)} + assert_equal(["10 not found"], ary.fetch_values(10) {|i| "#{i} not found"}) + assert_equal(["10 not found", 3], ary.fetch_values(10, 2) {|i| "#{i} not found"}) + end + def test_index2 a = [0, 1, 2] assert_equal(a, a.index.to_a) @@ -2976,13 +3041,12 @@ class TestArray < Test::Unit::TestCase end end - def test_shuffle_random - gen = proc do - 10000000 - end - class << gen - alias rand call - end + def test_shuffle_random_out_of_range + gen = random_generator {10000000} + assert_raise(RangeError) { + [*0..2].shuffle(random: gen) + } + gen = random_generator {-1} assert_raise(RangeError) { [*0..2].shuffle(random: gen) } @@ -2990,27 +3054,16 @@ class TestArray < Test::Unit::TestCase def test_shuffle_random_clobbering ary = (0...10000).to_a - gen = proc do + gen = random_generator do ary.replace([]) 0.5 end - class << gen - alias rand call - end assert_raise(RuntimeError) {ary.shuffle!(random: gen)} end def test_shuffle_random_zero - zero = Object.new - def zero.to_int - 0 - end - gen_to_int = proc do |max| - zero - end - class << gen_to_int - alias rand call - end + zero = Struct.new(:to_int).new(0) + gen_to_int = random_generator {|max| zero} ary = (0...10000).to_a assert_equal(ary.rotate, ary.shuffle(random: gen_to_int)) end @@ -3078,19 +3131,11 @@ class TestArray < Test::Unit::TestCase def test_sample_random_generator ary = (0...10000).to_a assert_raise(ArgumentError) {ary.sample(1, 2, random: nil)} - gen0 = proc do |max| - max/2 - end - class << gen0 - alias rand call - end - gen1 = proc do |max| + gen0 = random_generator {|max| max/2} + gen1 = random_generator do |max| ary.replace([]) max/2 end - class << gen1 - alias rand call - end assert_equal(5000, ary.sample(random: gen0)) assert_nil(ary.sample(random: gen1)) assert_equal([], ary) @@ -3121,20 +3166,23 @@ class TestArray < Test::Unit::TestCase end def test_sample_random_generator_half - half = Object.new - def half.to_int - 5000 - end - gen_to_int = proc do |max| - half - end - class << gen_to_int - alias rand call - end + half = Struct.new(:to_int).new(5000) + gen_to_int = random_generator {|max| half} ary = (0...10000).to_a assert_equal(5000, ary.sample(random: gen_to_int)) end + def test_sample_random_out_of_range + gen = random_generator {10000000} + assert_raise(RangeError) { + [*0..2].sample(random: gen) + } + gen = random_generator {-1} + assert_raise(RangeError) { + [*0..2].sample(random: gen) + } + end + def test_sample_random_invalid_generator ary = (0..10).to_a assert_raise(NoMethodError) { @@ -3336,6 +3384,8 @@ class TestArray < Test::Unit::TestCase assert_equal(nil, a.bsearch {|x| 1 * (2**100) }) assert_equal(nil, a.bsearch {|x| (-1) * (2**100) }) + assert_equal(4, a.bsearch {|x| (4 - x).to_r }) + assert_include([4, 7], a.bsearch {|x| (2**100).coerce((1 - x / 4) * (2**100)).first }) end @@ -3371,6 +3421,8 @@ class TestArray < Test::Unit::TestCase assert_equal(nil, a.bsearch_index {|x| 1 * (2**100) }) assert_equal(nil, a.bsearch_index {|x| (-1) * (2**100) }) + assert_equal(1, a.bsearch_index {|x| (4 - x).to_r }) + assert_include([1, 2], a.bsearch_index {|x| (2**100).coerce((1 - x / 4) * (2**100)).first }) end @@ -3460,6 +3512,17 @@ class TestArray < Test::Unit::TestCase assert_typed_equal(e, v, Complex, msg) end + def test_shrink_shared_array + assert_normal_exit(<<~'RUBY', '[Feature #20589]') + array = [] + # Make sure the array is allocated + 10.times { |i| array << i } + # Simulate a C extension using OBJ_FREEZE + Object.instance_method(:freeze).bind_call(array) + array.dup + RUBY + end + def test_sum assert_int_equal(0, [].sum) assert_int_equal(3, [3].sum) @@ -3534,11 +3597,45 @@ class TestArray < Test::Unit::TestCase assert_equal(10000, eval(lit).size) end + def test_array_safely_modified_by_sort_block + var_0 = (1..70).to_a + var_0.sort! do |var_0_block_129, var_1_block_129| + var_0.pop + var_1_block_129 <=> var_0_block_129 + end.shift(3) + 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) EnvUtil.suppress_warning {require 'continuation'} end + omit 'requires callcc support' unless respond_to?(:callcc, true) + end + + def random_generator(&block) + class << block + alias rand call + end + block end end diff --git a/test/ruby/test_assignment.rb b/test/ruby/test_assignment.rb index 3a8dafb7f0..3d0e773c82 100644 --- a/test/ruby/test_assignment.rb +++ b/test/ruby/test_assignment.rb @@ -248,6 +248,16 @@ class TestAssignment < Test::Unit::TestCase a,b,*c = *[*[1,2]]; assert_equal([1,2,[]], [a,b,c]) end + def test_massign_optimized_literal_bug_21012 + a = [] + def a.[]=(*args) + push args + end + a["a", "b"], = 1 + a["a", 10], = 2 + assert_equal [["a", "b", 1], ["a", 10, 2]], a + end + def test_assign_rescue a = raise rescue 2; assert_equal(2, a) a, b = raise rescue [3,4]; assert_equal([3, 4], [a, b]) diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index 875cf7138e..22ccbfb604 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -2,6 +2,7 @@ require 'test/unit' require 'tempfile' require 'pp' +require_relative '../lib/parser_support' class RubyVM module AbstractSyntaxTree @@ -47,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 @@ -66,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 @@ -134,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 @@ -214,7 +215,144 @@ class TestAst < Test::Unit::TestCase end end + def assert_parse(code, warning: '') + node = assert_warning(warning) {RubyVM::AbstractSyntaxTree.parse(code)} + assert_kind_of(RubyVM::AbstractSyntaxTree::Node, node, code) + end + + def assert_invalid_parse(msg, code) + assert_raise_with_message(SyntaxError, msg, code) do + RubyVM::AbstractSyntaxTree.parse(code) + end + end + + def test_invalid_exit + [ + "break", + "break true", + "next", + "next true", + "redo", + ].each do |code, *args| + msg = /Invalid #{code[/\A\w+/]}/ + assert_parse("while false; #{code}; end") + assert_parse("until true; #{code}; end") + assert_parse("begin #{code}; end while false") + assert_parse("begin #{code}; end until true") + assert_parse("->{#{code}}") + assert_parse("->{class X; #{code}; end}") + assert_invalid_parse(msg, "#{code}") + assert_invalid_parse(msg, "def m; #{code}; end") + assert_invalid_parse(msg, "begin; #{code}; end") + assert_parse("END {#{code}}") + + assert_parse("!defined?(#{code})") + assert_parse("def m; defined?(#{code}); end") + assert_parse("!begin; defined?(#{code}); end") + + next if code.include?(" ") + assert_parse("!defined? #{code}") + assert_parse("def m; defined? #{code}; end") + assert_parse("!begin; defined? #{code}; end") + end + end + + def test_invalid_retry + msg = /Invalid retry/ + assert_invalid_parse(msg, "retry") + assert_invalid_parse(msg, "def m; retry; end") + assert_invalid_parse(msg, "begin retry; end") + assert_parse("begin rescue; retry; end") + assert_invalid_parse(msg, "begin rescue; else; retry; end") + assert_invalid_parse(msg, "begin rescue; ensure; retry; end") + assert_parse("nil rescue retry") + assert_invalid_parse(msg, "END {retry}") + assert_invalid_parse(msg, "begin rescue; END {retry}; end") + + assert_parse("!defined?(retry)") + assert_parse("def m; defined?(retry); end") + assert_parse("!begin defined?(retry); end") + assert_parse("begin rescue; else; defined?(retry); end") + assert_parse("begin rescue; ensure; p defined?(retry); end") + assert_parse("END {defined?(retry)}") + assert_parse("begin rescue; END {defined?(retry)}; end") + assert_parse("!defined? retry") + + assert_parse("def m; defined? retry; end") + assert_parse("!begin defined? retry; end") + assert_parse("begin rescue; else; defined? retry; end") + assert_parse("begin rescue; ensure; p defined? retry; end") + assert_parse("END {defined? retry}") + assert_parse("begin rescue; END {defined? retry}; end") + + assert_parse("#{<<-"begin;"}\n#{<<-'end;'}") + begin; + def foo + begin + yield + rescue StandardError => e + begin + puts "hi" + retry + rescue + retry unless e + raise e + else + retry + ensure + retry + end + end + end + end; + end + + def test_invalid_yield + msg = /Invalid yield/ + assert_invalid_parse(msg, "yield") + assert_invalid_parse(msg, "class C; yield; end") + assert_invalid_parse(msg, "BEGIN {yield}") + assert_invalid_parse(msg, "END {yield}") + assert_invalid_parse(msg, "-> {yield}") + + assert_invalid_parse(msg, "yield true") + assert_invalid_parse(msg, "class C; yield true; end") + assert_invalid_parse(msg, "BEGIN {yield true}") + assert_invalid_parse(msg, "END {yield true}") + assert_invalid_parse(msg, "-> {yield true}") + + assert_parse("!defined?(yield)") + assert_parse("class C; defined?(yield); end") + assert_parse("BEGIN {defined?(yield)}") + assert_parse("END {defined?(yield)}") + + assert_parse("!defined?(yield true)") + assert_parse("class C; defined?(yield true); end") + assert_parse("BEGIN {defined?(yield true)}") + assert_parse("END {defined?(yield true)}") + + assert_parse("!defined? yield") + assert_parse("class C; defined? yield; end") + assert_parse("BEGIN {defined? yield}") + 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? + exception = begin raise rescue => e @@ -227,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]' @@ -234,6 +416,8 @@ class TestAst < Test::Unit::TestCase end def test_of_proc_and_method + omit if ParserSupport.prism_enabled? || ParserSupport.prism_enabled_in_subprocess? + proc = Proc.new { 1 + 2 } method = self.method(__method__) @@ -263,6 +447,8 @@ class TestAst < Test::Unit::TestCase end def test_of_backtrace_location + omit if ParserSupport.prism_enabled? + backtrace_location, lineno = sample_backtrace_location node = RubyVM::AbstractSyntaxTree.of(backtrace_location) assert_instance_of(RubyVM::AbstractSyntaxTree::Node, node) @@ -274,6 +460,8 @@ class TestAst < Test::Unit::TestCase end def test_of_proc_and_method_under_eval + omit if ParserSupport.prism_enabled? + keep_script_lines_back = RubyVM.keep_script_lines RubyVM.keep_script_lines = false @@ -303,6 +491,7 @@ class TestAst < Test::Unit::TestCase end def test_of_proc_and_method_under_eval_with_keep_script_lines + omit if ParserSupport.prism_enabled? pend if ENV['RUBY_ISEQ_DUMP_DEBUG'] # TODO keep_script_lines_back = RubyVM.keep_script_lines @@ -334,6 +523,8 @@ class TestAst < Test::Unit::TestCase end def test_of_backtrace_location_under_eval + omit if ParserSupport.prism_enabled? + keep_script_lines_back = RubyVM.keep_script_lines RubyVM.keep_script_lines = false @@ -352,6 +543,7 @@ class TestAst < Test::Unit::TestCase end def test_of_backtrace_location_under_eval_with_keep_script_lines + omit if ParserSupport.prism_enabled? pend if ENV['RUBY_ISEQ_DUMP_DEBUG'] # TODO keep_script_lines_back = RubyVM.keep_script_lines @@ -467,7 +659,7 @@ class TestAst < Test::Unit::TestCase assert_equal("foo", head) assert_equal(:EVSTR, body.type) body, = body.children - assert_equal(:LIT, body.type) + assert_equal(:INTEGER, body.type) assert_equal([1], body.children) end @@ -539,7 +731,7 @@ class TestAst < Test::Unit::TestCase node ? [node.children[-4], node.children[-2]&.children, node.children[-1]] : [] end - assert_equal([:*, nil, :&], forwarding.call('...')) + assert_equal([:*, [:**], :&], forwarding.call('...')) end def test_ranges_numbered_parameter @@ -574,6 +766,37 @@ class TestAst < Test::Unit::TestCase assert_equal(:a, args.children[rest]) end + def test_return + assert_ast_eqaul(<<~STR, <<~EXP) + def m(a) + return a + end + STR + (SCOPE@1:0-3:3 + tbl: [] + args: nil + body: + (DEFN@1:0-3:3 + mid: :m + body: + (SCOPE@1:0-3:3 + tbl: [:a] + args: + (ARGS@1:6-1:7 + pre_num: 1 + pre_init: nil + opt: nil + first_post: nil + post_num: 0 + post_init: nil + rest: nil + kw: nil + kwrest: nil + block: nil) + body: (RETURN@2:2-2:10 (LVAR@2:9-2:10 :a))))) + EXP + end + def test_keep_script_lines_for_parse node = RubyVM::AbstractSyntaxTree.parse(<<~END, keep_script_lines: true) 1.times do @@ -614,16 +837,41 @@ dummy end def test_keep_script_lines_for_of + omit if ParserSupport.prism_enabled? + proc = Proc.new { 1 + 2 } method = self.method(__method__) 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 + def test_keep_script_lines_for_of_with_existing_SCRIPT_LINES__that_has__FILE__as_a_key + omit if ParserSupport.prism_enabled? || ParserSupport.prism_enabled_in_subprocess? + + # This test confirms that the bug that previously occurred because of + # `AbstractSyntaxTree.of`s unnecessary dependence on SCRIPT_LINES__ does not reproduce. + # The bug occurred only if SCRIPT_LINES__ included __FILE__ as a key. + lines = [ + "SCRIPT_LINES__ = {__FILE__ => []}", + "puts RubyVM::AbstractSyntaxTree.of(->{ 1 + 2 }, keep_script_lines: true).script_lines", + "p SCRIPT_LINES__" + ] + test_stdout = lines + ['{"-e" => []}'] + assert_in_out_err(["-e", lines.join("\n")], "", test_stdout, []) + end + + def test_source_with_multibyte_characters + ast = RubyVM::AbstractSyntaxTree.parse(%{a("\u00a7");b("\u00a9")}, keep_script_lines: true) + a_fcall, b_fcall = ast.children[2].children + + assert_equal(%{a("\u00a7")}, a_fcall.source) + assert_equal(%{b("\u00a9")}, b_fcall.source) + end + def test_keep_tokens_for_parse node = RubyVM::AbstractSyntaxTree.parse(<<~END, keep_tokens: true) 1.times do @@ -645,6 +893,12 @@ dummy assert_equal(expected, node.all_tokens.map { [_2, _3]}) end + def test_keep_tokens_unexpected_backslash + assert_raise_with_message(SyntaxError, /unexpected backslash/) do + RubyVM::AbstractSyntaxTree.parse("\\", keep_tokens: true) + end + end + def test_encoding_with_keep_script_lines # Stop a warning "possibly useless use of a literal in void context" verbose_bak, $VERBOSE = $VERBOSE, nil @@ -665,8 +919,10 @@ dummy end def test_e_option + 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 @@ -749,7 +1005,7 @@ dummy begin a = 1 STR - (SCOPE@1:0-2:7 tbl: [:a] args: nil body: (LASGN@2:2-2:7 :a (LIT@2:6-2:7 1))) + (SCOPE@1:0-2:7 tbl: [:a] args: nil body: (LASGN@2:2-2:7 :a (INTEGER@2:6-2:7 1))) EXP end @@ -762,7 +1018,8 @@ dummy tbl: [:a] args: nil body: - (IF@1:0-2:7 (VCALL@1:3-1:7 :cond) (LASGN@2:2-2:7 :a (LIT@2:6-2:7 1)) nil)) + (IF@1:0-2:7 (VCALL@1:3-1:7 :cond) (LASGN@2:2-2:7 :a (INTEGER@2:6-2:7 1)) + nil)) EXP assert_error_tolerant(<<~STR, <<~EXP) @@ -774,7 +1031,7 @@ dummy tbl: [:a] args: nil body: - (IF@1:0-3:4 (VCALL@1:3-1:7 :cond) (LASGN@2:2-2:7 :a (LIT@2:6-2:7 1)) + (IF@1:0-3:4 (VCALL@1:3-1:7 :cond) (LASGN@2:2-2:7 :a (INTEGER@2:6-2:7 1)) (BEGIN@3:4-3:4 nil))) EXP end @@ -788,7 +1045,7 @@ dummy tbl: [:a] args: nil body: - (UNLESS@1:0-2:7 (VCALL@1:7-1:11 :cond) (LASGN@2:2-2:7 :a (LIT@2:6-2:7 1)) + (UNLESS@1:0-2:7 (VCALL@1:7-1:11 :cond) (LASGN@2:2-2:7 :a (INTEGER@2:6-2:7 1)) nil)) EXP @@ -801,7 +1058,7 @@ dummy tbl: [:a] args: nil body: - (UNLESS@1:0-3:4 (VCALL@1:7-1:11 :cond) (LASGN@2:2-2:7 :a (LIT@2:6-2:7 1)) + (UNLESS@1:0-3:4 (VCALL@1:7-1:11 :cond) (LASGN@2:2-2:7 :a (INTEGER@2:6-2:7 1)) (BEGIN@3:4-3:4 nil))) EXP end @@ -840,7 +1097,7 @@ dummy args: nil body: (CASE@1:0-2:6 (VCALL@1:5-1:6 :a) - (WHEN@2:0-2:6 (LIST@2:5-2:6 (LIT@2:5-2:6 1) nil) (BEGIN@2:6-2:6 nil) + (WHEN@2:0-2:6 (LIST@2:5-2:6 (INTEGER@2:5-2:6 1) nil) (BEGIN@2:6-2:6 nil) nil))) EXP @@ -857,7 +1114,7 @@ dummy (WHEN@2:0-2:11 (LIST@2:5-2:11 (OPCALL@2:5-2:11 (VCALL@2:5-2:6 :a) :== - (LIST@2:10-2:11 (LIT@2:10-2:11 1) nil)) nil) + (LIST@2:10-2:11 (INTEGER@2:10-2:11 1) nil)) nil) (BEGIN@2:11-2:11 nil) nil))) EXP @@ -876,7 +1133,7 @@ dummy const: nil kw: (HASH@2:4-2:13 - (LIST@2:4-2:13 (LIT@2:4-2:6 :a) (CONST@2:7-2:13 :String) nil)) + (LIST@2:4-2:13 (SYM@2:4-2:6 :a) (CONST@2:7-2:13 :String) nil)) kwrest: nil) (BEGIN@2:14-2:14 nil) nil))) EXP end @@ -958,7 +1215,7 @@ dummy tbl: [] args: nil body: - (ITER@1:0-2:3 (FCALL@1:0-1:3 :m (LIST@1:2-1:3 (LIT@1:2-1:3 1) nil)) + (ITER@1:0-2:3 (FCALL@1:0-1:3 :m (LIST@1:2-1:3 (INTEGER@1:2-1:3 1) nil)) (SCOPE@1:4-2:3 tbl: [] args: nil body: (VCALL@2:2-2:3 :a)))) EXP end @@ -973,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 @@ -1069,10 +1326,56 @@ dummy EXP end - def assert_error_tolerant(src, expected) + def test_error_tolerant_unexpected_backslash + node = assert_error_tolerant("\\", <<~EXP, keep_tokens: true) + (SCOPE@1:0-1:1 tbl: [] args: nil body: (ERROR@1:0-1:1)) + EXP + assert_equal([[0, :backslash, "\\", [1, 0, 1, 1]]], node.children.last.tokens) + end + + def test_with_bom + assert_error_tolerant("\u{feff}nil", <<~EXP) + (SCOPE@1:0-1:3 tbl: [] args: nil body: (NIL@1:0-1:3)) + EXP + end + + def test_unused_block_local_variable + assert_warning('') do + RubyVM::AbstractSyntaxTree.parse(%{->(; foo) {}}) + end + end + + def test_memory_leak + assert_no_memory_leak([], "#{<<~"begin;"}", "\n#{<<~'end;'}", rss: true) + begin; + 1_000_000.times do + eval("") + end + end; + end + + def test_locations + begin + verbose_bak, $VERBOSE = $VERBOSE, false + node = RubyVM::AbstractSyntaxTree.parse("1 + 2") + ensure + $VERBOSE = verbose_bak + end + locations = node.locations + + assert_equal(RubyVM::AbstractSyntaxTree::Location, locations[0].class) + end + + private + + def assert_error_tolerant(src, expected, keep_tokens: false) + assert_ast_eqaul(src, expected, error_tolerant: true, keep_tokens: keep_tokens) + end + + def assert_ast_eqaul(src, expected, **options) begin verbose_bak, $VERBOSE = $VERBOSE, false - node = RubyVM::AbstractSyntaxTree.parse(src, error_tolerant: true) + node = RubyVM::AbstractSyntaxTree.parse(src, **options) ensure $VERBOSE = verbose_bak end @@ -1080,5 +1383,371 @@ dummy str = "" PP.pp(node, str, 80) assert_equal(expected, str) + node + end + + class TestLocation < Test::Unit::TestCase + def test_lineno_and_column + node = ast_parse("1 + 2") + assert_locations(node.locations, [[1, 0, 1, 5]]) + end + + def test_alias_locations + node = ast_parse("alias foo bar") + assert_locations(node.children[-1].locations, [[1, 0, 1, 13], [1, 0, 1, 5]]) + end + + def test_and_locations + node = ast_parse("1 and 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 7], [1, 2, 1, 5]]) + + node = ast_parse("1 && 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 6], [1, 2, 1, 4]]) + end + + def test_block_pass_locations + node = ast_parse("foo(&bar)") + assert_locations(node.children[-1].children[-1].locations, [[1, 4, 1, 8], [1, 4, 1, 5]]) + + node = ast_parse("def a(&); b(&) end") + assert_locations(node.children[-1].children[-1].children[-1].children[-1].children[-1].locations, [[1, 12, 1, 13], [1, 12, 1, 13]]) + end + + def test_break_locations + node = ast_parse("loop { break 1 }") + assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 7, 1, 14], [1, 7, 1, 12]]) + end + + def test_case_locations + node = ast_parse("case a; when 1; end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 19], [1, 0, 1, 4], [1, 16, 1, 19]]) + end + + def test_case2_locations + node = ast_parse("case; when 1; end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 17], [1, 0, 1, 4], [1, 14, 1, 17]]) + end + + def test_case3_locations + node = ast_parse("case a; in 1; end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 17], [1, 0, 1, 4], [1, 14, 1, 17]]) + end + + def test_class_locations + node = ast_parse("class A end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 11], [1, 0, 1, 5], nil, [1, 8, 1, 11]]) + + node = ast_parse("class A < B; end") + 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]]) + + node = ast_parse("foo(1..2)") + assert_locations(node.children[-1].children[-1].children[0].locations, [[1, 4, 1, 8], [1, 5, 1, 7]]) + + node = ast_parse("foo(1..2, 3)") + assert_locations(node.children[-1].children[-1].children[0].locations, [[1, 4, 1, 8], [1, 5, 1, 7]]) + + node = ast_parse("foo(..2)") + assert_locations(node.children[-1].children[-1].children[0].locations, [[1, 4, 1, 7], [1, 4, 1, 6]]) + end + + def test_dot3_locations + node = ast_parse("1...2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 5], [1, 1, 1, 4]]) + + node = ast_parse("foo(1...2)") + assert_locations(node.children[-1].children[-1].children[0].locations, [[1, 4, 1, 9], [1, 5, 1, 8]]) + + node = ast_parse("foo(1...2, 3)") + assert_locations(node.children[-1].children[-1].children[0].locations, [[1, 4, 1, 9], [1, 5, 1, 8]]) + + node = ast_parse("foo(...2)") + assert_locations(node.children[-1].children[-1].children[0].locations, [[1, 4, 1, 8], [1, 4, 1, 7]]) + end + + def test_evstr_locations + node = ast_parse('"#{foo}"') + assert_locations(node.children[-1].children[1].locations, [[1, 0, 1, 8], [1, 1, 1, 3], [1, 6, 1, 7]]) + + node = ast_parse('"#$1"') + assert_locations(node.children[-1].children[1].locations, [[1, 0, 1, 5], [1, 1, 1, 2], nil]) + end + + def test_flip2_locations + node = ast_parse("if 'a'..'z'; foo; end") + assert_locations(node.children[-1].children[0].locations, [[1, 3, 1, 11], [1, 6, 1, 8]]) + + node = ast_parse('if 1..5; foo; end') + assert_locations(node.children[-1].children[0].locations, [[1, 3, 1, 7], [1, 4, 1, 6]]) + end + + def test_flip3_locations + node = ast_parse("if 'a'...('z'); foo; end") + assert_locations(node.children[-1].children[0].locations, [[1, 3, 1, 14], [1, 6, 1, 9]]) + + node = ast_parse('if 1...5; foo; end') + assert_locations(node.children[-1].children[0].locations, [[1, 3, 1, 8], [1, 4, 1, 7]]) + end + + def test_for_locations + node = ast_parse("for a in b; end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 15], [1, 0, 1, 3], [1, 6, 1, 8], nil, [1, 12, 1, 15]]) + + node = ast_parse("for a in b do; end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 18], [1, 0, 1, 3], [1, 6, 1, 8], [1, 11, 1, 13], [1, 15, 1, 18]]) + end + + def test_lambda_locations + node = ast_parse("-> (a, b) { foo }") + assert_locations(node.children[-1].locations, [[1, 0, 1, 17], [1, 0, 1, 2], [1, 10, 1, 11], [1, 16, 1, 17]]) + + node = ast_parse("-> (a, b) do foo end") + 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]]) + + node = ast_parse("1 if 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 6], [1, 2, 1, 4], nil, nil]) + + node = ast_parse("if 1; elsif 2; else end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 23], [1, 0, 1, 2], [1, 4, 1, 5], [1, 20, 1, 23]]) + assert_locations(node.children[-1].children[-1].locations, [[1, 6, 1, 19], [1, 6, 1, 11], [1, 13, 1, 14], [1, 20, 1, 23]]) + + node = ast_parse("true ? 1 : 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 12], nil, [1, 9, 1, 10], nil]) + + node = ast_parse("case a; in b if c; end") + 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]]) + end + + def test_op_asgn1_locations + node = ast_parse("ary[1] += foo") + assert_locations(node.children[-1].locations, [[1, 0, 1, 13], nil, [1, 3, 1, 4], [1, 5, 1, 6], [1, 7, 1, 9]]) + + node = ast_parse("ary[1, 2] += foo") + assert_locations(node.children[-1].locations, [[1, 0, 1, 16], nil, [1, 3, 1, 4], [1, 8, 1, 9], [1, 10, 1, 12]]) + end + + def test_or_locations + node = ast_parse("1 or 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 6], [1, 2, 1, 4]]) + + node = ast_parse("1 || 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 6], [1, 2, 1, 4]]) + end + + def test_op_asgn2_locations + node = ast_parse("a.b += 1") + assert_locations(node.children[-1].locations, [[1, 0, 1, 8], [1, 1, 1, 2], [1, 2, 1, 3], [1, 4, 1, 6]]) + + node = ast_parse("A::B.c += d") + assert_locations(node.children[-1].locations, [[1, 0, 1, 11], [1, 4, 1, 5], [1, 5, 1, 6], [1, 7, 1, 9]]) + + node = ast_parse("a = b.c += d") + assert_locations(node.children[-1].children[-1].locations, [[1, 4, 1, 12], [1, 5, 1, 6], [1, 6, 1, 7], [1, 8, 1, 10]]) + + node = ast_parse("a = A::B.c += d") + assert_locations(node.children[-1].children[-1].locations, [[1, 4, 1, 15], [1, 8, 1, 9], [1, 9, 1, 10], [1, 11, 1, 13]]) + end + + def test_postexe_locations + node = ast_parse("END { }") + assert_locations(node.children[-1].locations, [[1, 0, 1, 8], [1, 0, 1, 3], [1, 4, 1, 5], [1, 7, 1, 8]]) + + node = ast_parse("END { 1 }") + assert_locations(node.children[-1].locations, [[1, 0, 1, 9], [1, 0, 1, 3], [1, 4, 1, 5], [1, 8, 1, 9]]) + end + + def test_redo_locations + node = ast_parse("loop { redo }") + assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 7, 1, 11], [1, 7, 1, 11]]) + end + + def test_regx_locations + node = ast_parse("/foo/") + assert_locations(node.children[-1].locations, [[1, 0, 1, 5], [1, 0, 1, 1], [1, 1, 1, 4], [1, 4, 1, 5]]) + + node = ast_parse("/foo/i") + assert_locations(node.children[-1].locations, [[1, 0, 1, 6], [1, 0, 1, 1], [1, 1, 1, 4], [1, 4, 1, 6]]) + end + + def test_return_locations + node = ast_parse("return 1") + assert_locations(node.children[-1].locations, [[1, 0, 1, 8], [1, 0, 1, 6]]) + + node = ast_parse("return") + 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]]) + + node = ast_parse("a = *1, 2") + assert_locations(node.children[-1].children[1].children[0].locations, [[1, 4, 1, 6], [1, 4, 1, 5]]) + + node = ast_parse("case a; when *1; end") + assert_locations(node.children[-1].children[1].children[0].locations, [[1, 13, 1, 15], [1, 13, 1, 14]]) + + node = ast_parse("case a; when *1, 2; end") + assert_locations(node.children[-1].children[1].children[0].children[0].locations, [[1, 13, 1, 15], [1, 13, 1, 14]]) + end + + def test_super_locations + node = ast_parse("super 1") + assert_locations(node.children[-1].locations, [[1, 0, 1, 7], [1, 0, 1, 5], nil, nil]) + + node = ast_parse("super(1)") + assert_locations(node.children[-1].locations, [[1, 0, 1, 8], [1, 0, 1, 5], [1, 5, 1, 6], [1, 7, 1, 8]]) + end + + def test_unless_locations + node = ast_parse("unless cond then 1 else 2 end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 29], [1, 0, 1, 6], [1, 12, 1, 16], [1, 26, 1, 29]]) + + node = ast_parse("1 unless 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 10], [1, 2, 1, 8], nil, nil]) + end + + def test_undef_locations + node = ast_parse("undef foo") + assert_locations(node.children[-1].locations, [[1, 0, 1, 9], [1, 0, 1, 5]]) + + node = ast_parse("undef foo, bar") + assert_locations(node.children[-1].locations, [[1, 0, 1, 14], [1, 0, 1, 5]]) + end + + def test_valias_locations + node = ast_parse("alias $foo $bar") + assert_locations(node.children[-1].locations, [[1, 0, 1, 15], [1, 0, 1, 5]]) + + node = ast_parse("alias $foo $&") + assert_locations(node.children[-1].locations, [[1, 0, 1, 13], [1, 0, 1, 5]]) + + node = ast_parse("alias $foo $`") + assert_locations(node.children[-1].locations, [[1, 0, 1, 13], [1, 0, 1, 5]]) + + node = ast_parse("alias $foo $'") + assert_locations(node.children[-1].locations, [[1, 0, 1, 13], [1, 0, 1, 5]]) + + node = ast_parse("alias $foo $+") + assert_locations(node.children[-1].locations, [[1, 0, 1, 13], [1, 0, 1, 5]]) + end + + def test_when_locations + node = ast_parse("case a; when 1 then 2; end") + assert_locations(node.children[-1].children[1].locations, [[1, 8, 1, 22], [1, 8, 1, 12], [1, 15, 1, 19]]) + end + + def test_while_locations + node = ast_parse("while cond do 1 end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 19], [1, 0, 1, 5], [1, 16, 1, 19]]) + + node = ast_parse("1 while 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 9], [1, 2, 1, 7], nil]) + end + + def test_until_locations + node = ast_parse("until cond do 1 end") + assert_locations(node.children[-1].locations, [[1, 0, 1, 19], [1, 0, 1, 5], [1, 16, 1, 19]]) + + node = ast_parse("1 until 2") + assert_locations(node.children[-1].locations, [[1, 0, 1, 9], [1, 2, 1, 7], nil]) + end + + def test_yield_locations + node = ast_parse("def foo; yield end") + assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 9, 1, 14], [1, 9, 1, 14], nil, nil]) + + node = ast_parse("def foo; yield() end") + assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 9, 1, 16], [1, 9, 1, 14], [1, 14, 1, 15], [1, 15, 1, 16]]) + + node = ast_parse("def foo; yield 1, 2 end") + assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 9, 1, 19], [1, 9, 1, 14], nil, nil]) + + node = ast_parse("def foo; yield(1, 2) end") + assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 9, 1, 20], [1, 9, 1, 14], [1, 14, 1, 15], [1, 19, 1, 20]]) + end + + private + def ast_parse(src, **options) + begin + verbose_bak, $VERBOSE = $VERBOSE, nil + RubyVM::AbstractSyntaxTree.parse(src, **options) + ensure + $VERBOSE = verbose_bak + end + end + + def assert_locations(locations, expected) + ary = locations.map {|loc| loc && [loc.first_lineno, loc.first_column, loc.last_lineno, loc.last_column] } + + assert_equal(ary, expected) + end end end diff --git a/test/ruby/test_autoload.rb b/test/ruby/test_autoload.rb index e475520321..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,13 +280,18 @@ 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 end def test_bug_13526 + # Skip this on macOS 10.13 because of the following error: + # http://rubyci.s3.amazonaws.com/osx1013/ruby-master/log/20231011T014505Z.fail.html.gz + require "rbconfig" + script = File.join(__dir__, 'bug-13526.rb') assert_ruby_status([script], '', '[ruby-core:81016] [Bug #13526]') end @@ -556,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. @@ -595,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 bb0562c0bb..dad7dfcb55 100644 --- a/test/ruby/test_backtrace.rb +++ b/test/ruby/test_backtrace.rb @@ -155,6 +155,10 @@ class TestBacktrace < Test::Unit::TestCase end def test_each_backtrace_location + assert_nil(Thread.each_caller_location {}) + + assert_raise(LocalJumpError) {Thread.each_caller_location} + i = 0 cl = caller_locations(1, 1)[0]; ecl = Thread.each_caller_location{|x| i+=1; break x if i == 1} assert_equal(cl.to_s, ecl.to_s) @@ -181,6 +185,10 @@ class TestBacktrace < Test::Unit::TestCase assert_raise(StopIteration) { ecl.next } + + ary = [] + cl = caller_locations(1, 2); Thread.each_caller_location(1, 2) {|x| ary << x} + assert_equal(cl.map(&:to_s), ary.map(&:to_s)) end def test_caller_locations_first_label @@ -215,15 +223,15 @@ class TestBacktrace < Test::Unit::TestCase @res = caller_locations(2, 1).inspect end @line = __LINE__ + 1 - 1.times.map { 1.times.map { foo } } - assert_equal("[\"#{__FILE__}:#{@line}:in `times'\"]", @res) + [1].map.map { [1].map.map { foo } } + assert_equal("[\"#{__FILE__}:#{@line}:in 'Array#map'\"]", @res) end def test_caller_location_path_cfunc_iseq_no_pc def self.foo @res = caller_locations(2, 1)[0].path end - 1.times.map { 1.times.map { foo } } + [1].map.map { [1].map.map { foo } } assert_equal(__FILE__, @res) end @@ -292,13 +300,13 @@ class TestBacktrace < Test::Unit::TestCase end def test_caller_locations_label - assert_equal("#{__method__}", caller_locations(0, 1)[0].label) + assert_equal("TestBacktrace##{__method__}", caller_locations(0, 1)[0].label) loc, = tap {break caller_locations(0, 1)} - assert_equal("block in #{__method__}", loc.label) + assert_equal("block in TestBacktrace##{__method__}", loc.label) begin raise rescue - assert_equal("rescue in #{__method__}", caller_locations(0, 1)[0].label) + assert_equal("TestBacktrace##{__method__}", caller_locations(0, 1)[0].label) end end @@ -378,17 +386,17 @@ class TestBacktrace < Test::Unit::TestCase def test_core_backtrace_hash_merge e = assert_raise(TypeError) do - {**nil} + {**1} end assert_not_match(/\Acore#/, e.backtrace_locations[0].base_label) end def test_notty_backtrace - err = ["-:1:in `<main>': unhandled exception"] + err = ["-:1:in '<main>': unhandled exception"] assert_in_out_err([], "raise", [], err) - err = ["-:2:in `foo': foo! (RuntimeError)", - "\tfrom -:4:in `<main>'"] + err = ["-:2:in 'Object#foo': foo! (RuntimeError)", + "\tfrom -:4:in '<main>'"] assert_in_out_err([], <<-"end;", [], err) def foo raise "foo!" @@ -396,12 +404,11 @@ class TestBacktrace < Test::Unit::TestCase foo end; - err = ["-:7:in `rescue in bar': bar! (RuntimeError)", - "\tfrom -:4:in `bar'", - "\tfrom -:9:in `<main>'", - "-:2:in `foo': foo! (RuntimeError)", - "\tfrom -:5:in `bar'", - "\tfrom -:9:in `<main>'"] + err = ["-:7:in 'Object#bar': bar! (RuntimeError)", + "\tfrom -:9:in '<main>'", + "-:2:in 'Object#foo': foo! (RuntimeError)", + "\tfrom -:5:in 'Object#bar'", + "\tfrom -:9:in '<main>'"] assert_in_out_err([], <<-"end;", [], err) def foo raise "foo!" @@ -416,7 +423,7 @@ class TestBacktrace < Test::Unit::TestCase end def test_caller_to_enum - err = ["-:3:in `foo': unhandled exception", "\tfrom -:in `each'"] + err = ["-:3:in 'Object#foo': unhandled exception", "\tfrom -:in 'Enumerator#each'"] assert_in_out_err([], <<-"end;", [], err, "[ruby-core:91911]") def foo return to_enum(__method__) unless block_given? @@ -428,4 +435,35 @@ class TestBacktrace < Test::Unit::TestCase enum.next end; end + + def test_no_receiver_for_anonymous_class + err = ["-:2:in 'bar': unhandled exception", # Not '#<Class:0xXXX>.bar' + "\tfrom -:3:in '<main>'"] + assert_in_out_err([], <<-"end;", [], err) + foo = Class.new + def foo.bar = raise + foo.bar + end; + + err = ["-:3:in 'baz': unhandled exception", # Not '#<Class:0xXXX>::Bar.baz' + "\tfrom -:4:in '<main>'"] + assert_in_out_err([], <<-"end;", [], err) + foo = Class.new + foo::Bar = Class.new + def (foo::Bar).baz = raise + 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_beginendblock.rb b/test/ruby/test_beginendblock.rb index 2b281645b5..3706efab52 100644 --- a/test/ruby/test_beginendblock.rb +++ b/test/ruby/test_beginendblock.rb @@ -1,5 +1,6 @@ # frozen_string_literal: false require 'test/unit' +EnvUtil.suppress_warning {require 'continuation'} class TestBeginEndBlock < Test::Unit::TestCase DIR = File.dirname(File.expand_path(__FILE__)) @@ -45,9 +46,9 @@ class TestBeginEndBlock < Test::Unit::TestCase end def test_endblockwarn_in_eval - assert_in_out_err([], "#{<<~"begin;"}\n#{<<~'end;'}", [], ['(eval):2: warning: END in method; use at_exit']) + assert_in_out_err([], "#{<<~"begin;"}\n#{<<~'end;'}", [], ['test.rb:1: warning: END in method; use at_exit']) begin; - eval <<-EOE + eval <<-EOE, nil, "test.rb", 0 def end2 END {} end @@ -67,7 +68,7 @@ class TestBeginEndBlock < Test::Unit::TestCase bug8501 = '[ruby-core:55365] [Bug #8501]' args = ['-e', 'o = Object.new; def o.inspect; raise "[Bug #8501]"; end', '-e', 'at_exit{o.nope}'] - status = assert_in_out_err(args, '', [], /undefined method `nope'/, bug8501) + status = assert_in_out_err(args, '', [], /undefined method 'nope'/, bug8501) assert_not_predicate(status, :success?, bug8501) end @@ -131,6 +132,8 @@ class TestBeginEndBlock < Test::Unit::TestCase end def test_callcc_at_exit + omit 'requires callcc support' unless respond_to?(:callcc) + bug9110 = '[ruby-core:58329][Bug #9110]' assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}", bug9110) begin; diff --git a/test/ruby/test_bignum.rb b/test/ruby/test_bignum.rb index 065a944853..c366f794b2 100644 --- a/test/ruby/test_bignum.rb +++ b/test/ruby/test_bignum.rb @@ -207,7 +207,7 @@ class TestBignum < Test::Unit::TestCase assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; digits = [["3", 700], ["0", 2700], ["1", 1], ["0", 26599]] - num = digits.inject("") {|s,(c,n)|s << c*n}.to_i + num = digits.inject(+"") {|s,(c,n)|s << c*n}.to_i assert_equal digits.sum {|c,n|n}, num.to_s.size end; end @@ -476,8 +476,8 @@ class TestBignum < Test::Unit::TestCase def test_pow assert_equal(1.0, T32 ** 0.0) assert_equal(1.0 / T32, T32 ** -1) - assert_equal(1, assert_warning(/may be too big/) {T32 ** T32}.infinite?) - assert_equal(1, assert_warning(/may be too big/) {T32 ** (2**30-1)}.infinite?) + assert_raise(ArgumentError) { T32 ** T32 } + assert_raise(ArgumentError) { T32 ** (2**30-1) } ### rational changes the behavior of Bignum#** #assert_raise(TypeError) { T32**"foo" } @@ -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 @@ -821,5 +867,11 @@ class TestBignum < Test::Unit::TestCase assert_nil(T1024P.infinite?) assert_nil((-T1024P).infinite?) end + + def test_gmp_version + if RbConfig::CONFIG.fetch('configure_args').include?("'--with-gmp'") + assert_kind_of(String, Integer::GMP_VERSION) + end + end if ENV['GITHUB_WORKFLOW'] == 'Compilations' end end 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 dbdb0752be..dd1936c4e2 100644 --- a/test/ruby/test_call.rb +++ b/test/ruby/test_call.rb @@ -1,8 +1,26 @@ # frozen_string_literal: false require 'test/unit' +require '-test-/iter' class TestCall < Test::Unit::TestCase - def aaa(a, b=100, *rest) + # These dummy method definitions prevent warnings "the block passed to 'a'..." + def a(&) = nil + def b(&) = nil + def c(&) = nil + def d(&) = nil + def e(&) = nil + def f(&) = nil + def g(&) = nil + def h(&) = nil + def i(&) = nil + def j(&) = nil + def k(&) = nil + def l(&) = nil + def m(&) = nil + def n(&) = nil + def o(&) = nil + + def aaa(a, b=100, *rest, &) res = [a, b] res += rest if rest return res @@ -99,7 +117,229 @@ class TestCall < Test::Unit::TestCase } end - def test_call_splat_order + def test_frozen_splat_and_keywords + a = [1, 2].freeze + def self.f(*a); a end + 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) + ary = [10] + assert_equal(10, a(*ary)) + end + + def test_call_bmethod_proc_restarg + pr = proc{|*sym| sym} + define_singleton_method(:a, &pr) + ary = [10] + assert_equal([10], a(*ary)) + assert_equal([10], a(10)) + end + + def test_call_op_asgn_keywords + h = Class.new do + attr_reader :get, :set + def v; yield; [*@get, *@set] end + def [](*a, **b, &c) @get = [a, b, c]; @set = []; 3 end + def []=(*a, **b, &c) @set = [a, b, c] end + end.new + + a = [] + kw = {} + b = lambda{} + + # Prevent "assigned but unused variable" warnings + _ = [h, a, kw, b] + + message = /keyword arg given in index assignment/ + + # +=, without block, non-popped + assert_syntax_error(%q{h[**kw] += 1}, message) + assert_syntax_error(%q{h[0, **kw] += 1}, message) + assert_syntax_error(%q{h[0, *a, **kw] += 1}, message) + assert_syntax_error(%q{h[kw: 5] += 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] += 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] += 1}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2] += 1}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, nil: 3] += 1}, message) + + # +=, with block, non-popped + assert_syntax_error(%q{h[**kw, &b] += 1}, message) + assert_syntax_error(%q{h[0, **kw, &b] += 1}, message) + assert_syntax_error(%q{h[0, *a, **kw, &b] += 1}, message) + assert_syntax_error(%q{h[kw: 5, &b] += 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] += 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] += 1}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2, &b] += 1}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, b: 3, &b] += 1}, message) + + # +=, without block, popped + assert_syntax_error(%q{h[**kw] += 1; nil}, message) + assert_syntax_error(%q{h[0, **kw] += 1; nil}, message) + assert_syntax_error(%q{h[0, *a, **kw] += 1; nil}, message) + assert_syntax_error(%q{h[kw: 5] += 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] += 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] += 1; nil}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2] += 1; nil}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, nil: 3] += 1; nil}, message) + + # +=, with block, popped + assert_syntax_error(%q{h[**kw, &b] += 1; nil}, message) + assert_syntax_error(%q{h[0, **kw, &b] += 1; nil}, message) + assert_syntax_error(%q{h[0, *a, **kw, &b] += 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, &b] += 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] += 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] += 1; nil}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2, &b] += 1; nil}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, b: 3, &b] += 1; nil}, message) + + # &&=, without block, non-popped + assert_syntax_error(%q{h[**kw] &&= 1}, message) + assert_syntax_error(%q{h[0, **kw] &&= 1}, message) + assert_syntax_error(%q{h[0, *a, **kw] &&= 1}, message) + assert_syntax_error(%q{h[kw: 5] &&= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] &&= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] &&= 1}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2] &&= 1}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, nil: 3] &&= 1}, message) + + # &&=, with block, non-popped + assert_syntax_error(%q{h[**kw, &b] &&= 1}, message) + assert_syntax_error(%q{h[0, **kw, &b] &&= 1}, message) + assert_syntax_error(%q{h[0, *a, **kw, &b] &&= 1}, message) + assert_syntax_error(%q{h[kw: 5, &b] &&= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] &&= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] &&= 1}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2, &b] &&= 1}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, b: 3, &b] &&= 1}, message) + + # &&=, without block, popped + assert_syntax_error(%q{h[**kw] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, **kw] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, **kw] &&= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5] &&= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] &&= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, nil: 3] &&= 1; nil}, message) + + # &&=, with block, popped + assert_syntax_error(%q{h[**kw, &b] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, **kw, &b] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, **kw, &b] &&= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, &b] &&= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] &&= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2, &b] &&= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, b: 3, &b] &&= 1; nil}, message) + + # ||=, without block, non-popped + assert_syntax_error(%q{h[**kw] ||= 1}, message) + assert_syntax_error(%q{h[0, **kw] ||= 1}, message) + assert_syntax_error(%q{h[0, *a, **kw] ||= 1}, message) + assert_syntax_error(%q{h[kw: 5] ||= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] ||= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] ||= 1}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2] ||= 1}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, nil: 3] ||= 1}, message) + + # ||=, with block, non-popped + assert_syntax_error(%q{h[**kw, &b] ||= 1}, message) + assert_syntax_error(%q{h[0, **kw, &b] ||= 1}, message) + assert_syntax_error(%q{h[0, *a, **kw, &b] ||= 1}, message) + assert_syntax_error(%q{h[kw: 5, &b] ||= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] ||= 1}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] ||= 1}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2, &b] ||= 1}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, b: 3, &b] ||= 1}, message) + + # ||=, without block, popped + assert_syntax_error(%q{h[**kw] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, **kw] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, **kw] ||= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5] ||= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] ||= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, nil: 3] ||= 1; nil}, message) + + # ||=, with block, popped + assert_syntax_error(%q{h[**kw, &b] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, **kw, &b] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, **kw, &b] ||= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, &b] ||= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] ||= 1; nil}, message) + assert_syntax_error(%q{h[kw: 5, a: 2, &b] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, kw: 5, a: 2, &b] ||= 1; nil}, message) + assert_syntax_error(%q{h[0, *a, kw: 5, a: 2, b: 3, &b] ||= 1; nil}, message) + + end + + def test_kwsplat_block_order_op_asgn + o = Object.new + ary = [] + o.define_singleton_method(:to_a) {ary << :to_a; []} + o.define_singleton_method(:to_hash) {ary << :to_hash; {}} + o.define_singleton_method(:to_proc) {ary << :to_proc; lambda{}} + + def o.[](...) 2 end + def o.[]=(...) end + + message = /keyword arg given in index assignment/ + + assert_syntax_error(%q{o[kw: 1] += 1}, message) + assert_syntax_error(%q{o[**o] += 1}, message) + assert_syntax_error(%q{o[**o, &o] += 1}, message) + assert_syntax_error(%q{o[*o, **o, &o] += 1}, message) + end + + def test_call_op_asgn_keywords_mutable + h = Class.new do + attr_reader :get, :set + def v; yield; [*@get, *@set] end + def [](*a, **b) + @get = [a.dup, b.dup] + a << :splat_modified + b[:kw_splat_modified] = true + @set = [] + 3 + end + def []=(*a, **b) @set = [a, b] end + end.new + + message = /keyword arg given in index assignment/ + + a = [] + kw = {} + + # Prevent "assigned but unused variable" warnings + _ = [h, a, kw] + + assert_syntax_error(%q{h[*a, 2, b: 5, **kw] += 1}, message) + end + + def test_call_splat_post_order bug12860 = '[ruby-core:77701] [Bug# 12860]' ary = [1, 2] assert_equal([1, 2, 1], aaa(*ary, ary.shift), bug12860) @@ -107,7 +347,7 @@ class TestCall < Test::Unit::TestCase assert_equal([0, 1, 2, 1], aaa(0, *ary, ary.shift), bug12860) end - def test_call_block_order + def test_call_splat_block_order bug16504 = '[ruby-core:96769] [Bug# 16504]' b = proc{} ary = [1, 2, b] @@ -116,8 +356,178 @@ class TestCall < Test::Unit::TestCase assert_equal([0, 1, 2, b], aaa(0, *ary, &ary.pop), bug16504) end + def test_call_splat_kw_order + b = {} + ary = [1, 2, b] + assert_equal([1, 2, b, {a: b}], aaa(*ary, a: ary.pop)) + ary = [1, 2, b] + assert_equal([0, 1, 2, b, {a: b}], aaa(0, *ary, a: ary.pop)) + end + + def test_call_splat_kw_splat_order + b = {} + ary = [1, 2, b] + assert_equal([1, 2, b], aaa(*ary, **ary.pop)) + ary = [1, 2, b] + assert_equal([0, 1, 2, b], aaa(0, *ary, **ary.pop)) + end + + def test_call_args_splat_with_nonhash_keyword_splat + o = Object.new + def o.to_hash; {a: 1} end + def self.f(*a, **kw) + kw + end + assert_equal Hash, f(*[], **o).class + end + + def test_call_args_splat_with_pos_arg_kw_splat_is_not_mutable + o = Object.new + def o.foo(a, **h)= h[:splat_modified] = true + + a = [] + b = {splat_modified: false} + + o.foo(*a, :x, **b) + + assert_equal({splat_modified: false}, b) + end + + def test_anon_splat + r2kh = Hash.ruby2_keywords_hash(kw: 2) + r2kea = [r2kh] + r2ka = [1, r2kh] + + def self.s(*) ->(*a){a}.call(*) end + assert_equal([], s) + assert_equal([1], s(1)) + assert_equal([{kw: 2}], s(kw: 2)) + assert_equal([{kw: 2}], s(**{kw: 2})) + assert_equal([1, {kw: 2}], s(1, kw: 2)) + assert_equal([1, {kw: 2}], s(1, **{kw: 2})) + assert_equal([{kw: 2}], s(*r2kea)) + assert_equal([1, {kw: 2}], s(*r2ka)) + + singleton_class.remove_method(:s) + def self.s(*, kw: 0) [*->(*a){a}.call(*), kw] end + assert_equal([0], s) + assert_equal([1, 0], s(1)) + assert_equal([2], s(kw: 2)) + assert_equal([2], s(**{kw: 2})) + assert_equal([1, 2], s(1, kw: 2)) + assert_equal([1, 2], s(1, **{kw: 2})) + assert_equal([2], s(*r2kea)) + assert_equal([1, 2], s(*r2ka)) + + singleton_class.remove_method(:s) + def self.s(*, **kw) [*->(*a){a}.call(*), kw] end + assert_equal([{}], s) + assert_equal([1, {}], s(1)) + assert_equal([{kw: 2}], s(kw: 2)) + assert_equal([{kw: 2}], s(**{kw: 2})) + assert_equal([1, {kw: 2}], s(1, kw: 2)) + assert_equal([1, {kw: 2}], s(1, **{kw: 2})) + assert_equal([{kw: 2}], s(*r2kea)) + assert_equal([1, {kw: 2}], s(*r2ka)) + + singleton_class.remove_method(:s) + def self.s(*, kw: 0, **kws) [*->(*a){a}.call(*), kw, kws] end + assert_equal([0, {}], s) + assert_equal([1, 0, {}], s(1)) + assert_equal([2, {}], s(kw: 2)) + assert_equal([2, {}], s(**{kw: 2})) + assert_equal([1, 2, {}], s(1, kw: 2)) + assert_equal([1, 2, {}], s(1, **{kw: 2})) + assert_equal([2, {}], s(*r2kea)) + 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 + + pr = ->{} + h = {a: pr} + a = [] + + ary = t(**h, &h.delete(:a)) + assert_equal([{a: pr}, pr], ary) + + h = {a: pr} + ary = t(*a, **h, &h.delete(:a)) + assert_equal([{a: pr}, pr], ary) + end + + def test_kwsplat_block_order + o = Object.new + ary = [] + o.define_singleton_method(:to_a) {ary << :to_a; []} + o.define_singleton_method(:to_hash) {ary << :to_hash; {}} + o.define_singleton_method(:to_proc) {ary << :to_proc; lambda{}} + + def self.t(...) end + + t(**o, &o) + assert_equal([:to_hash, :to_proc], ary) + + ary.clear + t(*o, **o, &o) + assert_equal([:to_a, :to_hash, :to_proc], ary) + end + + def test_kwsplat_block_order_super + def self.t(splat) + o = Object.new + ary = [] + o.define_singleton_method(:to_a) {ary << :to_a; []} + o.define_singleton_method(:to_hash) {ary << :to_hash; {}} + o.define_singleton_method(:to_proc) {ary << :to_proc; lambda{}} + if splat + super(*o, **o, &o) + else + super(**o, &o) + end + ary + end + extend Module.new{def t(...) end} + + assert_equal([:to_hash, :to_proc], t(false)) + assert_equal([:to_a, :to_hash, :to_proc], t(true)) + end + + OVER_STACK_LEN = (ENV['RUBY_OVER_STACK_LEN'] || 150).to_i # Greater than VM_ARGC_STACK_MAX + OVER_STACK_ARGV = OVER_STACK_LEN.times.to_a.freeze + def test_call_cfunc_splat_large_array_bug_4040 - a = 1380.times.to_a # Greater than VM_ARGC_STACK_MAX + a = OVER_STACK_ARGV assert_equal(a, [].push(*a)) assert_equal(a, [].push(a[0], *a[1..])) @@ -199,4 +609,881 @@ class TestCall < Test::Unit::TestCase # Not all tests use such a large array to reduce testing time. assert_equal(1380888, [].push(*1380888.times.to_a).size) end + + def test_call_iseq_large_array_splat_fail + def self.a; end + def self.b(a=1); end + def self.c(k: 1); end + def self.d(**kw); end + def self.e(k: 1, **kw); end + def self.f(a=1, k: 1); end + def self.g(a=1, **kw); end + def self.h(a=1, k: 1, **kw); end + + (:a..:h).each do |meth| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + instance_eval("#{meth}(*OVER_STACK_ARGV)", __FILE__, __LINE__) + end + end + end + + def test_call_iseq_large_array_splat_pass + def self.a(*a); a.length end + assert_equal OVER_STACK_LEN, a(*OVER_STACK_ARGV) + + def self.b(_, *a); a.length end + assert_equal (OVER_STACK_LEN - 1), b(*OVER_STACK_ARGV) + + def self.c(_, *a, _); a.length end + assert_equal (OVER_STACK_LEN - 2), c(*OVER_STACK_ARGV) + + def self.d(b=1, *a); a.length end + assert_equal (OVER_STACK_LEN - 1), d(*OVER_STACK_ARGV) + + def self.e(b=1, *a, _); a.length end + assert_equal (OVER_STACK_LEN - 2), e(*OVER_STACK_ARGV) + + def self.f(b, *a); a.length end + assert_equal (OVER_STACK_LEN - 1), f(*OVER_STACK_ARGV) + + def self.g(*a, k: 1); a.length end + assert_equal OVER_STACK_LEN, g(*OVER_STACK_ARGV) + + def self.h(*a, **kw); a.length end + assert_equal OVER_STACK_LEN, h(*OVER_STACK_ARGV) + + def self.i(*a, k: 1, **kw); a.length end + assert_equal OVER_STACK_LEN, i(*OVER_STACK_ARGV) + + def self.j(b=1, *a, k: 1); a.length end + assert_equal (OVER_STACK_LEN - 1), j(*OVER_STACK_ARGV) + + def self.k(b=1, *a, **kw); a.length end + assert_equal (OVER_STACK_LEN - 1), k(*OVER_STACK_ARGV) + + def self.l(b=1, *a, k: 1, **kw); a.length end + assert_equal (OVER_STACK_LEN - 1), l(*OVER_STACK_ARGV) + + def self.m(b=1, *a, _, k: 1); a.length end + assert_equal (OVER_STACK_LEN - 2), m(*OVER_STACK_ARGV) + + def self.n(b=1, *a, _, **kw); a.length end + assert_equal (OVER_STACK_LEN - 2), n(*OVER_STACK_ARGV) + + def self.o(b=1, *a, _, k: 1, **kw); a.length end + assert_equal (OVER_STACK_LEN - 2), o(*OVER_STACK_ARGV) + end + + def test_call_iseq_large_array_splat_with_large_number_of_parameters + args = OVER_STACK_ARGV.map{|i| "a#{i}"}.join(',') + args1 = (OVER_STACK_LEN-1).times.map{|i| "a#{i}"}.join(',') + + singleton_class.class_eval("def a(#{args}); [#{args}] end") + assert_equal OVER_STACK_ARGV, a(*OVER_STACK_ARGV) + + singleton_class.class_eval("def b(#{args}, b=0); [#{args}, b] end") + assert_equal(OVER_STACK_ARGV + [0], b(*OVER_STACK_ARGV)) + + singleton_class.class_eval("def c(#{args}, *b); [#{args}, b] end") + assert_equal(OVER_STACK_ARGV + [[]], c(*OVER_STACK_ARGV)) + + singleton_class.class_eval("def d(#{args1}, *b); [#{args1}, b] end") + assert_equal(OVER_STACK_ARGV[0...-1] + [[OVER_STACK_ARGV.last]], d(*OVER_STACK_ARGV)) + end if OVER_STACK_LEN < 200 + + def test_call_proc_large_array_splat_pass + [ + proc{0} , + proc{|a=1|a}, + proc{|k: 1|0}, + proc{|**kw| 0}, + proc{|k: 1, **kw| 0}, + proc{|a=1, k: 1| a}, + proc{|a=1, **kw| a}, + proc{|a=1, k: 1, **kw| a}, + ].each do |l| + assert_equal 0, l.call(*OVER_STACK_ARGV) + end + + assert_equal OVER_STACK_LEN, proc{|*a| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), proc{|_, *a| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), proc{|_, *a, _| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), proc{|b=1, *a| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), proc{|b=1, *a, _| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), proc{|b=1, *a| a.length}.(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, proc{|*a, k: 1| a.length}.(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, proc{|*a, **kw| a.length}.(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, proc{|*a, k: 1, **kw| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), proc{|b=1, *a, k: 1| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), proc{|b=1, *a, **kw| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), proc{|b=1, *a, k: 1, **kw| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), proc{|b=1, *a, _, k: 1| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), proc{|b=1, *a, _, **kw| a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), proc{|b=1, *a, _, k: 1, **kw| a.length}.(*OVER_STACK_ARGV) + end + + def test_call_proc_large_array_splat_with_large_number_of_parameters + args = OVER_STACK_ARGV.map{|i| "a#{i}"}.join(',') + args1 = (OVER_STACK_LEN-1).times.map{|i| "a#{i}"}.join(',') + + l = instance_eval("proc{|#{args}| [#{args}]}") + assert_equal OVER_STACK_ARGV, l.(*OVER_STACK_ARGV) + + l = instance_eval("proc{|#{args}, b| [#{args}, b]}") + assert_equal(OVER_STACK_ARGV + [nil], l.(*OVER_STACK_ARGV)) + + l = instance_eval("proc{|#{args1}| [#{args1}]}") + assert_equal(OVER_STACK_ARGV[0...-1], l.(*OVER_STACK_ARGV)) + + l = instance_eval("proc{|#{args}, *b| [#{args}, b]}") + assert_equal(OVER_STACK_ARGV + [[]], l.(*OVER_STACK_ARGV)) + + l = instance_eval("proc{|#{args1}, *b| [#{args1}, b]}") + assert_equal(OVER_STACK_ARGV[0...-1] + [[OVER_STACK_ARGV.last]], l.(*OVER_STACK_ARGV)) + + l = instance_eval("proc{|#{args}, b, *c| [#{args}, b, c]}") + assert_equal(OVER_STACK_ARGV + [nil, []], l.(*OVER_STACK_ARGV)) + + l = instance_eval("proc{|#{args}, b, *c, d| [#{args}, b, c, d]}") + assert_equal(OVER_STACK_ARGV + [nil, [], nil], l.(*OVER_STACK_ARGV)) + end if OVER_STACK_LEN < 200 + + def test_call_lambda_large_array_splat_fail + [ + ->{} , + ->(a=1){}, + ->(k: 1){}, + ->(**kw){}, + ->(k: 1, **kw){}, + ->(a=1, k: 1){}, + ->(a=1, **kw){}, + ->(a=1, k: 1, **kw){}, + ].each do |l| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + l.call(*OVER_STACK_ARGV) + end + end + end + + def test_call_lambda_large_array_splat_pass + assert_equal OVER_STACK_LEN, ->(*a){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), ->(_, *a){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), ->(_, *a, _){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), ->(b=1, *a){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), ->(b=1, *a, _){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), ->(b, *a){a.length}.(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, ->(*a, k: 1){a.length}.(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, ->(*a, **kw){a.length}.(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, ->(*a, k: 1, **kw){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), ->(b=1, *a, k: 1){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), ->(b=1, *a, **kw){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 1), ->(b=1, *a, k: 1, **kw){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), ->(b=1, *a, _, k: 1){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), ->(b=1, *a, _, **kw){a.length}.(*OVER_STACK_ARGV) + assert_equal (OVER_STACK_LEN - 2), ->(b=1, *a, _, k: 1, **kw){a.length}.(*OVER_STACK_ARGV) + end + + def test_call_yield_block_large_array_splat_pass + def self.a + yield(*OVER_STACK_ARGV) + end + + [ + proc{0} , + proc{|a=1|a}, + proc{|k: 1|0}, + proc{|**kw| 0}, + proc{|k: 1, **kw| 0}, + proc{|a=1, k: 1| a}, + proc{|a=1, **kw| a}, + proc{|a=1, k: 1, **kw| a}, + ].each do |l| + assert_equal 0, a(&l) + end + + assert_equal OVER_STACK_LEN, a{|*a| a.length} + assert_equal (OVER_STACK_LEN - 1), a{|_, *a| a.length} + assert_equal (OVER_STACK_LEN - 2), a{|_, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 1), a{|b=1, *a| a.length} + assert_equal (OVER_STACK_LEN - 2), a{|b=1, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 1), a{|b, *a| a.length} + assert_equal OVER_STACK_LEN, a{|*a, k: 1| a.length} + assert_equal OVER_STACK_LEN, a{|*a, **kw| a.length} + assert_equal OVER_STACK_LEN, a{|*a, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), a{|b=1, *a, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 1), a{|b=1, *a, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), a{|b=1, *a, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), a{|b=1, *a, _, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 2), a{|b=1, *a, _, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), a{|b=1, *a, _, k: 1, **kw| a.length} + end + + def test_call_yield_large_array_splat_with_large_number_of_parameters + def self.a + yield(*OVER_STACK_ARGV) + end + + args = OVER_STACK_ARGV.map{|i| "a#{i}"}.join(',') + args1 = (OVER_STACK_LEN-1).times.map{|i| "a#{i}"}.join(',') + + assert_equal OVER_STACK_ARGV, instance_eval("a{|#{args}| [#{args}]}", __FILE__, __LINE__) + assert_equal(OVER_STACK_ARGV + [nil], instance_eval("a{|#{args}, b| [#{args}, b]}", __FILE__, __LINE__)) + assert_equal(OVER_STACK_ARGV[0...-1], instance_eval("a{|#{args1}| [#{args1}]}", __FILE__, __LINE__)) + assert_equal(OVER_STACK_ARGV + [[]], instance_eval("a{|#{args}, *b| [#{args}, b]}", __FILE__, __LINE__)) + assert_equal(OVER_STACK_ARGV[0...-1] + [[OVER_STACK_ARGV.last]], instance_eval("a{|#{args1}, *b| [#{args1}, b]}", __FILE__, __LINE__)) + assert_equal(OVER_STACK_ARGV + [nil, []], instance_eval("a{|#{args}, b, *c| [#{args}, b, c]}", __FILE__, __LINE__)) + assert_equal(OVER_STACK_ARGV + [nil, [], nil], instance_eval("a{|#{args}, b, *c, d| [#{args}, b, c, d]}", __FILE__, __LINE__)) + end if OVER_STACK_LEN < 200 + + def test_call_yield_lambda_large_array_splat_fail + def self.a + yield(*OVER_STACK_ARGV) + end + [ + ->{} , + ->(a=1){}, + ->(k: 1){}, + ->(**kw){}, + ->(k: 1, **kw){}, + ->(a=1, k: 1){}, + ->(a=1, **kw){}, + ->(a=1, k: 1, **kw){}, + ].each do |l| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + a(&l) + end + end + end + + def test_call_yield_lambda_large_array_splat_pass + def self.a + yield(*OVER_STACK_ARGV) + end + + assert_equal OVER_STACK_LEN, a(&->(*a){a.length}) + assert_equal (OVER_STACK_LEN - 1), a(&->(_, *a){a.length}) + assert_equal (OVER_STACK_LEN - 2), a(&->(_, *a, _){a.length}) + assert_equal (OVER_STACK_LEN - 1), a(&->(b=1, *a){a.length}) + assert_equal (OVER_STACK_LEN - 2), a(&->(b=1, *a, _){a.length}) + assert_equal (OVER_STACK_LEN - 1), a(&->(b, *a){a.length}) + assert_equal OVER_STACK_LEN, a(&->(*a, k: 1){a.length}) + assert_equal OVER_STACK_LEN, a(&->(*a, **kw){a.length}) + assert_equal OVER_STACK_LEN, a(&->(*a, k: 1, **kw){a.length}) + assert_equal (OVER_STACK_LEN - 1), a(&->(b=1, *a, k: 1){a.length}) + assert_equal (OVER_STACK_LEN - 1), a(&->(b=1, *a, **kw){a.length}) + assert_equal (OVER_STACK_LEN - 1), a(&->(b=1, *a, k: 1, **kw){a.length}) + assert_equal (OVER_STACK_LEN - 2), a(&->(b=1, *a, _, k: 1){a.length}) + assert_equal (OVER_STACK_LEN - 2), a(&->(b=1, *a, _, **kw){a.length}) + assert_equal (OVER_STACK_LEN - 2), a(&->(b=1, *a, _, k: 1, **kw){a.length}) + end + + def test_call_send_iseq_large_array_splat_fail + def self.a; end + def self.b(a=1); end + def self.c(k: 1); end + def self.d(**kw); end + def self.e(k: 1, **kw); end + def self.f(a=1, k: 1); end + def self.g(a=1, **kw); end + def self.h(a=1, k: 1, **kw); end + + (:a..:h).each do |meth| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + send(meth, *OVER_STACK_ARGV) + end + end + end + + def test_call_send_iseq_large_array_splat_pass + def self.a(*a); a.length end + assert_equal OVER_STACK_LEN, send(:a, *OVER_STACK_ARGV) + + def self.b(_, *a); a.length end + assert_equal (OVER_STACK_LEN - 1), send(:b, *OVER_STACK_ARGV) + + def self.c(_, *a, _); a.length end + assert_equal (OVER_STACK_LEN - 2), send(:c, *OVER_STACK_ARGV) + + def self.d(b=1, *a); a.length end + assert_equal (OVER_STACK_LEN - 1), send(:d, *OVER_STACK_ARGV) + + def self.e(b=1, *a, _); a.length end + assert_equal (OVER_STACK_LEN - 2), send(:e, *OVER_STACK_ARGV) + + def self.f(b, *a); a.length end + assert_equal (OVER_STACK_LEN - 1), send(:f, *OVER_STACK_ARGV) + + def self.g(*a, k: 1); a.length end + assert_equal OVER_STACK_LEN, send(:g, *OVER_STACK_ARGV) + + def self.h(*a, **kw); a.length end + assert_equal OVER_STACK_LEN, send(:h, *OVER_STACK_ARGV) + + def self.i(*a, k: 1, **kw); a.length end + assert_equal OVER_STACK_LEN, send(:i, *OVER_STACK_ARGV) + + def self.j(b=1, *a, k: 1); a.length end + assert_equal (OVER_STACK_LEN - 1), send(:j, *OVER_STACK_ARGV) + + def self.k(b=1, *a, **kw); a.length end + assert_equal (OVER_STACK_LEN - 1), send(:k, *OVER_STACK_ARGV) + + def self.l(b=1, *a, k: 1, **kw); a.length end + assert_equal (OVER_STACK_LEN - 1), send(:l, *OVER_STACK_ARGV) + + def self.m(b=1, *a, _, k: 1); a.length end + assert_equal (OVER_STACK_LEN - 2), send(:m, *OVER_STACK_ARGV) + + def self.n(b=1, *a, _, **kw); a.length end + assert_equal (OVER_STACK_LEN - 2), send(:n, *OVER_STACK_ARGV) + + def self.o(b=1, *a, _, k: 1, **kw); a.length end + assert_equal (OVER_STACK_LEN - 2), send(:o, *OVER_STACK_ARGV) + end + + def test_call_send_iseq_large_array_splat_with_large_number_of_parameters + args = OVER_STACK_ARGV.map{|i| "a#{i}"}.join(',') + args1 = (OVER_STACK_LEN-1).times.map{|i| "a#{i}"}.join(',') + + singleton_class.class_eval("def a(#{args}); [#{args}] end") + assert_equal OVER_STACK_ARGV, send(:a, *OVER_STACK_ARGV) + + singleton_class.class_eval("def b(#{args}, b=0); [#{args}, b] end") + assert_equal(OVER_STACK_ARGV + [0], send(:b, *OVER_STACK_ARGV)) + + singleton_class.class_eval("def c(#{args}, *b); [#{args}, b] end") + assert_equal(OVER_STACK_ARGV + [[]], send(:c, *OVER_STACK_ARGV)) + + singleton_class.class_eval("def d(#{args1}, *b); [#{args1}, b] end") + assert_equal(OVER_STACK_ARGV[0...-1] + [[OVER_STACK_ARGV.last]], send(:d, *OVER_STACK_ARGV)) + end if OVER_STACK_LEN < 200 + + def test_call_send_cfunc_large_array_splat_fail + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + send(:object_id, *OVER_STACK_ARGV) + end + end + + def test_call_send_cfunc_large_array_splat_pass + assert_equal OVER_STACK_LEN, [].send(:push, *OVER_STACK_ARGV).length + end + + def test_call_attr_reader_large_array_splat_fail + singleton_class.send(:attr_reader, :a) + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + a(*OVER_STACK_ARGV) + end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + send(:a, *OVER_STACK_ARGV) + end + end + + def test_call_attr_writer_large_array_splat_fail + singleton_class.send(:attr_writer, :a) + singleton_class.send(:alias_method, :a, :a=) + + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 1)") do + a(*OVER_STACK_ARGV) + end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 1)") do + send(:a, *OVER_STACK_ARGV) + end + end + + def test_call_struct_aref_large_array_splat_fail + s = Struct.new(:a).new + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + s.a(*OVER_STACK_ARGV) + end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + s.send(:a, *OVER_STACK_ARGV) + end + end + + def test_call_struct_aset_large_array_splat_fail + s = Struct.new(:a) do + alias b a= + end.new + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 1)") do + s.b(*OVER_STACK_ARGV) + end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 1)") do + s.send(:b, *OVER_STACK_ARGV) + end + end + + def test_call_alias_large_array_splat + c = Class.new do + def a; end + def c(*a); a.length end + attr_accessor :e + end + sc = Class.new(c) do + alias b a + alias d c + alias f e + alias g e= + end + + obj = sc.new + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + obj.b(*OVER_STACK_ARGV) + end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + obj.f(*OVER_STACK_ARGV) + end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 1)") do + obj.g(*OVER_STACK_ARGV) + end + + assert_equal OVER_STACK_LEN, obj.d(*OVER_STACK_ARGV) + end + + def test_call_zsuper_large_array_splat + c = Class.new do + private + def a; end + def c(*a); a.length end + attr_reader :e + end + sc = Class.new(c) do + public :a + public :c + public :e + end + + obj = sc.new + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + obj.a(*OVER_STACK_ARGV) + end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + obj.e(*OVER_STACK_ARGV) + end + + assert_equal OVER_STACK_LEN, obj.c(*OVER_STACK_ARGV) + end + + class RefinedModuleLargeArrayTest + c = self + using(Module.new do + refine c do + def a; end + def c(*a) a.length end + attr_reader :e + end + end) + + def b + a(*OVER_STACK_ARGV) + end + + def d + c(*OVER_STACK_ARGV) + end + + def f + e(*OVER_STACK_ARGV) + end + end + + def test_call_refined_large_array_splat_fail + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + RefinedModuleLargeArrayTest.new.b + end + + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN}, expected 0)") do + RefinedModuleLargeArrayTest.new.f + end + end + + def test_call_refined_large_array_splat_pass + assert_equal OVER_STACK_LEN, RefinedModuleLargeArrayTest.new.d + end + + def test_call_method_missing_iseq_large_array_splat_fail + def self.method_missing(_) end + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN+1}, expected 1)") do + nonexistent_method(*OVER_STACK_ARGV) + end + + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN+1}, expected 1)") do + send(:nonexistent_method, *OVER_STACK_ARGV) + end + + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN+1}, expected 1)") do + send("nonexistent_method123", *OVER_STACK_ARGV) + end + end + + def test_call_method_missing_iseq_large_array_splat_pass + def self.method_missing(m, *a) + a.length + end + assert_equal OVER_STACK_LEN, nonexistent_method(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, send(:nonexistent_method, *OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, send("nonexistent_method123", *OVER_STACK_ARGV) + end + + def test_call_bmethod_large_array_splat_fail + define_singleton_method(:a){} + define_singleton_method(:b){|a=1|} + define_singleton_method(:c){|k: 1|} + define_singleton_method(:d){|**kw|} + define_singleton_method(:e){|k: 1, **kw|} + define_singleton_method(:f){|a=1, k: 1|} + define_singleton_method(:g){|a=1, **kw|} + define_singleton_method(:h){|a=1, k: 1, **kw|} + + (:a..:h).each do |meth| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + instance_eval("#{meth}(*OVER_STACK_ARGV)", __FILE__, __LINE__) + end + end + end + + def test_call_bmethod_large_array_splat_pass + define_singleton_method(:a){|*a| a.length} + assert_equal OVER_STACK_LEN, a(*OVER_STACK_ARGV) + + define_singleton_method(:b){|_, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), b(*OVER_STACK_ARGV) + + define_singleton_method(:c){|_, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 2), c(*OVER_STACK_ARGV) + + define_singleton_method(:d){|b=1, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), d(*OVER_STACK_ARGV) + + define_singleton_method(:e){|b=1, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 2), e(*OVER_STACK_ARGV) + + define_singleton_method(:f){|b, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), f(*OVER_STACK_ARGV) + + define_singleton_method(:g){|*a, k: 1| a.length} + assert_equal OVER_STACK_LEN, g(*OVER_STACK_ARGV) + + define_singleton_method(:h){|*a, **kw| a.length} + assert_equal OVER_STACK_LEN, h(*OVER_STACK_ARGV) + + define_singleton_method(:i){|*a, k: 1, **kw| a.length} + assert_equal OVER_STACK_LEN, i(*OVER_STACK_ARGV) + + define_singleton_method(:j){|b=1, *a, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 1), j(*OVER_STACK_ARGV) + + define_singleton_method(:k){|b=1, *a, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), k(*OVER_STACK_ARGV) + + define_singleton_method(:l){|b=1, *a, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), l(*OVER_STACK_ARGV) + + define_singleton_method(:m){|b=1, *a, _, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 2), m(*OVER_STACK_ARGV) + + define_singleton_method(:n){|b=1, *a, _, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), n(*OVER_STACK_ARGV) + + define_singleton_method(:o){|b=1, *a, _, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), o(*OVER_STACK_ARGV) + end + + def test_call_method_missing_bmethod_large_array_splat_fail + define_singleton_method(:method_missing){|_|} + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN+1}, expected 1)") do + nonexistent_method(*OVER_STACK_ARGV) + end + + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN+1}, expected 1)") do + send(:nonexistent_method, *OVER_STACK_ARGV) + end + + assert_raise_with_message(ArgumentError, "wrong number of arguments (given #{OVER_STACK_LEN+1}, expected 1)") do + send("nonexistent_method123", *OVER_STACK_ARGV) + end + end + + def test_call_method_missing_bmethod_large_array_splat_pass + define_singleton_method(:method_missing){|_, *a| a.length} + assert_equal OVER_STACK_LEN, nonexistent_method(*OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, send(:nonexistent_method, *OVER_STACK_ARGV) + assert_equal OVER_STACK_LEN, send("nonexistent_method123", *OVER_STACK_ARGV) + end + + def test_call_symproc_large_array_splat_fail + define_singleton_method(:a){} + define_singleton_method(:b){|a=1|} + define_singleton_method(:c){|k: 1|} + define_singleton_method(:d){|**kw|} + define_singleton_method(:e){|k: 1, **kw|} + define_singleton_method(:f){|a=1, k: 1|} + define_singleton_method(:g){|a=1, **kw|} + define_singleton_method(:h){|a=1, k: 1, **kw|} + + (:a..:h).each do |meth| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + instance_eval(":#{meth}.to_proc.(self, *OVER_STACK_ARGV)", __FILE__, __LINE__) + end + end + end + + def test_call_symproc_large_array_splat_pass + define_singleton_method(:a){|*a| a.length} + assert_equal OVER_STACK_LEN, :a.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:b){|_, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), :b.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:c){|_, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 2), :c.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:d){|b=1, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), :d.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:e){|b=1, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 2), :e.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:f){|b, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), :f.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:g){|*a, k: 1| a.length} + assert_equal OVER_STACK_LEN, :g.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:h){|*a, **kw| a.length} + assert_equal OVER_STACK_LEN, :h.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:i){|*a, k: 1, **kw| a.length} + assert_equal OVER_STACK_LEN, :i.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:j){|b=1, *a, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 1), :j.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:k){|b=1, *a, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), :k.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:l){|b=1, *a, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), :l.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:m){|b=1, *a, _, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 2), :m.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:n){|b=1, *a, _, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), :n.to_proc.(self, *OVER_STACK_ARGV) + + define_singleton_method(:o){|b=1, *a, _, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), :o.to_proc.(self, *OVER_STACK_ARGV) + end + + def test_call_rb_call_iseq_large_array_splat_fail + extend Bug::Iter::Yield + l = ->(*a){} + + def self.a; end + def self.b(a=1) end + def self.c(k: 1) end + def self.d(**kw) end + def self.e(k: 1, **kw) end + def self.f(a=1, k: 1) end + def self.g(a=1, **kw) end + def self.h(a=1, k: 1, **kw) end + + (:a..:h).each do |meth| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + yield_block(meth, *OVER_STACK_ARGV, &l) + end + end + end + + def test_call_rb_call_iseq_large_array_splat_pass + extend Bug::Iter::Yield + l = ->(*a){a.length} + + def self.a(*a) a.length end + assert_equal OVER_STACK_LEN, yield_block(:a, *OVER_STACK_ARGV, &l) + + def self.b(_, *a) a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:b, *OVER_STACK_ARGV, &l) + + def self.c(_, *a, _) a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:c, *OVER_STACK_ARGV, &l) + + def self.d(b=1, *a) a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:d, *OVER_STACK_ARGV, &l) + + def self.e(b=1, *a, _) a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:e, *OVER_STACK_ARGV, &l) + + def self.f(b, *a) a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:f, *OVER_STACK_ARGV, &l) + + def self.g(*a, k: 1) a.length end + assert_equal OVER_STACK_LEN, yield_block(:g, *OVER_STACK_ARGV, &l) + + def self.h(*a, **kw) a.length end + assert_equal OVER_STACK_LEN, yield_block(:h, *OVER_STACK_ARGV, &l) + + def self.i(*a, k: 1, **kw) a.length end + assert_equal OVER_STACK_LEN, yield_block(:h, *OVER_STACK_ARGV, &l) + + def self.j(b=1, *a, k: 1) a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:j, *OVER_STACK_ARGV, &l) + + def self.k(b=1, *a, **kw) a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:k, *OVER_STACK_ARGV, &l) + + def self.l(b=1, *a, k: 1, **kw) a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:l, *OVER_STACK_ARGV, &l) + + def self.m(b=1, *a, _, k: 1) a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:m, *OVER_STACK_ARGV, &l) + + def self.n(b=1, *a, _, **kw) a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:n, *OVER_STACK_ARGV, &l) + + def self.o(b=1, *a, _, k: 1, **kw) a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:o, *OVER_STACK_ARGV, &l) + end + + def test_call_rb_call_bmethod_large_array_splat_fail + extend Bug::Iter::Yield + l = ->(*a){} + + define_singleton_method(:a){||} + define_singleton_method(:b){|a=1|} + define_singleton_method(:c){|k: 1|} + define_singleton_method(:d){|**kw|} + define_singleton_method(:e){|k: 1, **kw|} + define_singleton_method(:f){|a=1, k: 1|} + define_singleton_method(:g){|a=1, **kw|} + define_singleton_method(:h){|a=1, k: 1, **kw|} + + (:a..:h).each do |meth| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + yield_block(meth, *OVER_STACK_ARGV, &l) + end + end + end + + def test_call_rb_call_bmethod_large_array_splat_pass + extend Bug::Iter::Yield + l = ->(*a){a.length} + + define_singleton_method(:a){|*a| a.length} + assert_equal OVER_STACK_LEN, yield_block(:a, *OVER_STACK_ARGV, &l) + + define_singleton_method(:b){|_, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), yield_block(:b, *OVER_STACK_ARGV, &l) + + define_singleton_method(:c){|_, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 2), yield_block(:c, *OVER_STACK_ARGV, &l) + + define_singleton_method(:d){|b=1, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), yield_block(:d, *OVER_STACK_ARGV, &l) + + define_singleton_method(:e){|b=1, *a, _| a.length} + assert_equal (OVER_STACK_LEN - 2), yield_block(:e, *OVER_STACK_ARGV, &l) + + define_singleton_method(:f){|b, *a| a.length} + assert_equal (OVER_STACK_LEN - 1), yield_block(:f, *OVER_STACK_ARGV, &l) + + define_singleton_method(:g){|*a, k: 1| a.length} + assert_equal OVER_STACK_LEN, yield_block(:g, *OVER_STACK_ARGV, &l) + + define_singleton_method(:h){|*a, **kw| a.length} + assert_equal OVER_STACK_LEN, yield_block(:h, *OVER_STACK_ARGV, &l) + + define_singleton_method(:i){|*a, k: 1, **kw| a.length} + assert_equal OVER_STACK_LEN, yield_block(:h, *OVER_STACK_ARGV, &l) + + define_singleton_method(:j){|b=1, *a, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 1), yield_block(:j, *OVER_STACK_ARGV, &l) + + define_singleton_method(:k){|b=1, *a, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), yield_block(:k, *OVER_STACK_ARGV, &l) + + define_singleton_method(:l){|b=1, *a, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 1), yield_block(:l, *OVER_STACK_ARGV, &l) + + define_singleton_method(:m){|b=1, *a, _, k: 1| a.length} + assert_equal (OVER_STACK_LEN - 2), yield_block(:m, *OVER_STACK_ARGV, &l) + + define_singleton_method(:n){|b=1, *a, _, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), yield_block(:n, *OVER_STACK_ARGV, &l) + + define_singleton_method(:o){|b=1, *a, _, k: 1, **kw| a.length} + assert_equal (OVER_STACK_LEN - 2), yield_block(:o, *OVER_STACK_ARGV, &l) + end + + def test_call_ifunc_iseq_large_array_splat_fail + extend Bug::Iter::Yield + def self.a(*a) + yield(*a) + end + [ + ->(){}, + ->(a=1){}, + ->(k: 1){}, + ->(**kw){}, + ->(k: 1, **kw){}, + ->(a=1, k: 1){}, + ->(a=1, **kw){}, + ->(a=1, k: 1, **kw){}, + ].each do |l| + assert_raise_with_message(ArgumentError, /wrong number of arguments \(given #{OVER_STACK_LEN}, expected 0(\.\.[12])?\)/) do + yield_block(:a, *OVER_STACK_ARGV, &l) + end + end + end + + def test_call_ifunc_iseq_large_array_splat_pass + extend Bug::Iter::Yield + def self.a(*a) + yield(*a) + end + + l = ->(*a) do a.length end + assert_equal OVER_STACK_LEN, yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(_, *a) do a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(_, *a, _) do a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a) do a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a, _) do a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b, *a) do a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(*a, k: 1) do a.length end + assert_equal OVER_STACK_LEN, yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(*a, **kw) do a.length end + assert_equal OVER_STACK_LEN, yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(*a, k: 1, **kw) do a.length end + assert_equal OVER_STACK_LEN, yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a, k: 1) do a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a, **kw) do a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a, k: 1, **kw) do a.length end + assert_equal (OVER_STACK_LEN - 1), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a, _, k: 1) do a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a, _, **kw) do a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:a, *OVER_STACK_ARGV, &l) + + l = ->(b=1, *a, _, k: 1, **kw) do a.length end + assert_equal (OVER_STACK_LEN - 2), yield_block(:a, *OVER_STACK_ARGV, &l) + end end diff --git a/test/ruby/test_case.rb b/test/ruby/test_case.rb index 4a0f1bf78d..9e8502fb27 100644 --- a/test/ruby/test_case.rb +++ b/test/ruby/test_case.rb @@ -68,10 +68,13 @@ class TestCase < Test::Unit::TestCase assert(false) end - assert_raise(NameError) do - case - when false, *x, false + begin + verbose_bak, $VERBOSE = $VERBOSE, nil + assert_raise(NameError) do + eval("case; when false, *x, false; end") end + ensure + $VERBOSE = verbose_bak end end diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb index 5086e60398..8f12e06685 100644 --- a/test/ruby/test_class.rb +++ b/test/ruby/test_class.rb @@ -96,6 +96,13 @@ class TestClass < Test::Unit::TestCase def test_superclass_of_basicobject assert_equal(nil, BasicObject.superclass) + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + module Mod end + BasicObject.include(Mod) + assert_equal(nil, BasicObject.superclass) + end; end def test_module_function @@ -252,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) } @@ -276,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 @@ -309,6 +352,7 @@ class TestClass < Test::Unit::TestCase def test_invalid_return_from_class_definition assert_syntax_error("class C; return; end", /Invalid return/) + assert_syntax_error("class << Object; return; end", /Invalid return/) end def test_invalid_yield_from_class_definition @@ -355,6 +399,17 @@ class TestClass < Test::Unit::TestCase assert_equal(42, PrivateClass.new.foo) end + def test_private_const_access + assert_raise_with_message NameError, /uninitialized/ do + begin + eval('class ::TestClass::PrivateClass; end') + rescue NameError + end + + Object.const_get "NOT_AVAILABLE_CONST_NAME_#{__LINE__}" + end + end + StrClone = String.clone Class.new(StrClone) @@ -546,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 @@ -681,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 @@ -702,9 +759,13 @@ class TestClass < Test::Unit::TestCase assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}") begin; - Date = (class C\u{1f5ff}; self; end).new + module Bug + module Class + TestClassDefinedInC = (class C\u{1f5ff}; self; end).new + end + end assert_raise_with_message(TypeError, /C\u{1f5ff}/) { - require 'date' + require '-test-/class' } end; end @@ -771,15 +832,15 @@ class TestClass < Test::Unit::TestCase c.attached_object end - assert_raise_with_message(TypeError, /`NilClass' is not a singleton class/) do + assert_raise_with_message(TypeError, /'NilClass' is not a singleton class/) do nil.singleton_class.attached_object end - assert_raise_with_message(TypeError, /`FalseClass' is not a singleton class/) do + assert_raise_with_message(TypeError, /'FalseClass' is not a singleton class/) do false.singleton_class.attached_object end - assert_raise_with_message(TypeError, /`TrueClass' is not a singleton class/) do + assert_raise_with_message(TypeError, /'TrueClass' is not a singleton class/) do true.singleton_class.attached_object end end @@ -818,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_clone.rb b/test/ruby/test_clone.rb index 216eaa39d2..775c9ed848 100644 --- a/test/ruby/test_clone.rb +++ b/test/ruby/test_clone.rb @@ -73,6 +73,13 @@ class TestClone < Test::Unit::TestCase assert_equal(cloned_obj.instance_variable_get(:@a), 1) end + def test_proc_obj_id_flag_reset + # [Bug #20250] + proc = Proc.new { } + proc.object_id + proc.clone.object_id # Would crash with RUBY_DEBUG=1 + end + def test_user_flags assert_separately([], <<-EOS) # diff --git a/test/ruby/test_comparable.rb b/test/ruby/test_comparable.rb index 4a90d443bf..b689469d9e 100644 --- a/test/ruby/test_comparable.rb +++ b/test/ruby/test_comparable.rb @@ -85,6 +85,12 @@ class TestComparable < Test::Unit::TestCase assert_equal(1, @o.clamp(1, 1)) assert_equal(@o, @o.clamp(0, 0)) + assert_equal(@o, @o.clamp(nil, 2)) + assert_equal(-2, @o.clamp(nil, -2)) + assert_equal(@o, @o.clamp(-2, nil)) + assert_equal(2, @o.clamp(2, nil)) + assert_equal(@o, @o.clamp(nil, nil)) + assert_raise_with_message(ArgumentError, 'min argument must be less than or equal to max argument') { @o.clamp(2, 1) } diff --git a/test/ruby/test_compile_prism.rb b/test/ruby/test_compile_prism.rb new file mode 100644 index 0000000000..76b961b37e --- /dev/null +++ b/test/ruby/test_compile_prism.rb @@ -0,0 +1,2755 @@ +# frozen_string_literal: true + +# This file is organized to match itemization in https://github.com/ruby/prism/issues/1335 +module Prism + class TestCompilePrism < Test::Unit::TestCase + def test_iseq_has_node_id + code = "proc { <<END }\n hello\nEND" + iseq = RubyVM::InstructionSequence.compile_prism(code) + assert_operator iseq.to_a[4][:node_id], :>, -1 + end + + # Subclass is used for tests which need it + class Subclass; end + ############################################################################ + # Literals # + ############################################################################ + + def test_FalseNode + assert_prism_eval("false") + end + + def test_FloatNode + assert_prism_eval("1.2") + assert_prism_eval("1.2e3") + assert_prism_eval("+1.2e+3") + assert_prism_eval("-1.2e-3") + end + + def test_ImaginaryNode + assert_prism_eval("1i") + assert_prism_eval("+1.0i") + assert_prism_eval("1ri") + end + + def test_IntegerNode + assert_prism_eval("1") + assert_prism_eval("+1") + assert_prism_eval("-1") + assert_prism_eval("0x10") + assert_prism_eval("0b10") + assert_prism_eval("0o10") + assert_prism_eval("010") + assert_prism_eval("(0o00)") + end + + def test_NilNode + assert_prism_eval("nil") + end + + def test_RationalNode + assert_prism_eval("1.2r") + assert_prism_eval("+1.2r") + end + + def test_SelfNode + assert_prism_eval("self") + end + + def test_SourceEncodingNode + assert_prism_eval("__ENCODING__") + end + + def test_SourceFileNode + assert_prism_eval("__FILE__") + end + + def test_SourceLineNode + assert_prism_eval("__LINE__", raw: true) + end + + def test_TrueNode + assert_prism_eval("true") + end + + ############################################################################ + # Reads # + ############################################################################ + + def test_BackReferenceReadNode + assert_prism_eval("$+") + end + + def test_ClassVariableReadNode + assert_prism_eval("class Prism::TestCompilePrism; @@pit = 1; @@pit; end") + end + + def test_ConstantPathNode + assert_prism_eval("Prism::TestCompilePrism") + end + + def test_ConstantReadNode + assert_prism_eval("Prism") + end + + Z = 1 + + def test_DefinedNode + assert_prism_eval("defined? nil") + assert_prism_eval("defined? self") + assert_prism_eval("defined? true") + assert_prism_eval("defined? false") + assert_prism_eval("defined? 1") + assert_prism_eval("defined? 1i") + assert_prism_eval("defined? 1.0") + assert_prism_eval("defined? 1..2") + assert_prism_eval("defined? [A, B, C]") + assert_prism_eval("defined? [1, 2, 3]") + assert_prism_eval("defined?({ a: 1 })") + assert_prism_eval("defined? 'str'") + assert_prism_eval('defined?("#{expr}")') + assert_prism_eval("defined? :sym") + assert_prism_eval("defined? /foo/") + assert_prism_eval('defined?(/#{1}/)') + assert_prism_eval("defined? -> { 1 + 1 }") + assert_prism_eval("defined? a && b") + assert_prism_eval("defined? a || b") + assert_prism_eval("defined? __ENCODING__") + assert_prism_eval("defined? __FILE__") + assert_prism_eval("defined? __LINE__") + + assert_prism_eval("defined? %[1,2,3]") + assert_prism_eval("defined? %q[1,2,3]") + assert_prism_eval("defined? %Q[1,2,3]") + assert_prism_eval("defined? %r[1,2,3]") + assert_prism_eval("defined? %i[1,2,3]") + assert_prism_eval("defined? %I[1,2,3]") + assert_prism_eval("defined? %w[1,2,3]") + assert_prism_eval("defined? %W[1,2,3]") + assert_prism_eval("defined? %s[1,2,3]") + assert_prism_eval("defined? %x[1,2,3]") + + assert_prism_eval("defined? [*b]") + assert_prism_eval("defined? [[*1..2], 3, *4..5]") + assert_prism_eval("defined? [a: [:b, :c]]") + assert_prism_eval("defined? 1 in 1") + + assert_prism_eval("defined? @a") + assert_prism_eval("defined? $a") + assert_prism_eval("defined? @@a") + assert_prism_eval("defined? A") + assert_prism_eval("defined? ::A") + assert_prism_eval("defined? A::B") + assert_prism_eval("defined? A::B::C") + assert_prism_eval("defined? #{self.class.name}::Z::A") + assert_prism_eval("defined? yield") + assert_prism_eval("defined? super") + + assert_prism_eval("defined? X = 1") + assert_prism_eval("defined? X *= 1") + assert_prism_eval("defined? X /= 1") + assert_prism_eval("defined? X &= 1") + assert_prism_eval("defined? X ||= 1") + + assert_prism_eval("defined? $1") + assert_prism_eval("defined? $2") + assert_prism_eval("defined? $`") + assert_prism_eval("defined? $'") + assert_prism_eval("defined? $+") + + assert_prism_eval("defined? $X = 1") + assert_prism_eval("defined? $X *= 1") + assert_prism_eval("defined? $X /= 1") + assert_prism_eval("defined? $X &= 1") + assert_prism_eval("defined? $X ||= 1") + + assert_prism_eval("defined? @@X = 1") + assert_prism_eval("defined? @@X *= 1") + assert_prism_eval("defined? @@X /= 1") + assert_prism_eval("defined? @@X &= 1") + assert_prism_eval("defined? @@X ||= 1") + + assert_prism_eval("defined? @X = 1") + assert_prism_eval("defined? @X *= 1") + assert_prism_eval("defined? @X /= 1") + assert_prism_eval("defined? @X &= 1") + assert_prism_eval("defined? @X ||= 1") + + assert_prism_eval("x = 1; defined? x = 1") + assert_prism_eval("x = 1; defined? x *= 1") + assert_prism_eval("x = 1; defined? x /= 1") + assert_prism_eval("x = 1; defined? x &= 1") + assert_prism_eval("x = 1; defined? x ||= 1") + + assert_prism_eval("if defined? A; end") + + assert_prism_eval("defined?(())") + assert_prism_eval("defined?(('1'))") + + # method chain starting with self that's truthy + assert_prism_eval("defined?(self.itself.itself.itself)") + + # method chain starting with self that's false (exception swallowed) + assert_prism_eval("defined?(self.itself.itself.neat)") + + # single self with method, truthy + assert_prism_eval("defined?(self.itself)") + + # single self with method, false + assert_prism_eval("defined?(self.neat!)") + + # method chain implicit self that's truthy + assert_prism_eval("defined?(itself.itself.itself)") + + # method chain implicit self that's false + assert_prism_eval("defined?(itself.neat.itself)") + + ## single method implicit self that's truthy + assert_prism_eval("defined?(itself)") + + ## single method implicit self that's false + assert_prism_eval("defined?(neatneat)") + + assert_prism_eval("defined?(a(itself))") + assert_prism_eval("defined?(itself(itself))") + + # method chain with a block on the inside + assert_prism_eval("defined?(itself { 1 }.itself)") + + # method chain with parenthesized receiver + assert_prism_eval("defined?((itself).itself)") + + # Method chain on a constant + assert_prism_eval(<<~RUBY) + class PrismDefinedNode + def m1; end + end + + defined?(PrismDefinedNode.new.m1) + RUBY + + assert_prism_eval("defined?(next)") + assert_prism_eval("defined?(break)") + assert_prism_eval("defined?(redo)") + assert_prism_eval("defined?(retry)") + + assert_prism_eval(<<~RUBY) + class PrismDefinedReturnNode + def self.m1; defined?(return) end + end + + PrismDefinedReturnNode.m1 + RUBY + + assert_prism_eval("defined?(begin; 1; end)") + + assert_prism_eval("defined?(defined?(a))") + assert_prism_eval('defined?(:"#{1}")') + assert_prism_eval("defined?(`echo #{1}`)") + + assert_prism_eval("defined?(PrismTestSubclass.test_call_and_write_node &&= 1)") + assert_prism_eval("defined?(PrismTestSubclass.test_call_operator_write_node += 1)") + assert_prism_eval("defined?(PrismTestSubclass.test_call_or_write_node ||= 1)") + assert_prism_eval("defined?(Prism::CPAWN &&= 1)") + assert_prism_eval("defined?(Prism::CPOWN += 1)") + assert_prism_eval("defined?(Prism::CPOrWN ||= 1)") + assert_prism_eval("defined?(Prism::CPWN = 1)") + assert_prism_eval("defined?([0][0] &&= 1)") + assert_prism_eval("defined?([0][0] += 1)") + assert_prism_eval("defined?([0][0] ||= 1)") + + assert_prism_eval("defined?(case :a; when :a; 1; else; 2; end)") + assert_prism_eval("defined?(case [1, 2, 3]; in [1, 2, 3]; 4; end)") + assert_prism_eval("defined?(class PrismClassA; end)") + assert_prism_eval("defined?(def prism_test_def_node; end)") + assert_prism_eval("defined?(for i in [1,2] do; i; end)") + assert_prism_eval("defined?(if true; 1; end)") + assert_prism_eval("defined?(/(?<foo>bar)/ =~ 'barbar')") + assert_prism_eval("defined?(1 => 1)") + assert_prism_eval("defined?(module M; end)") + assert_prism_eval("defined?(1.2r)") + assert_prism_eval("defined?(class << self; end)") + assert_prism_eval("defined?(while a != 1; end)") + assert_prism_eval("defined?(until a == 1; end)") + assert_prism_eval("defined?(unless true; 1; end)") + end + + def test_GlobalVariableReadNode + assert_prism_eval("$pit = 1; $pit") + end + + def test_InstanceVariableReadNode + assert_prism_eval("class Prism::TestCompilePrism; @pit = 1; @pit; end") + end + + def test_LocalVariableReadNode + assert_prism_eval("pit = 1; pit") + end + + def test_NumberedReferenceReadNode + assert_prism_eval("$1") + assert_prism_eval("$99999") + end + + ############################################################################ + # Writes # + ############################################################################ + + def test_ClassVariableAndWriteNode + assert_prism_eval("class Prism::TestCompilePrism; @@pit = 0; @@pit &&= 1; end") + end + + def test_ClassVariableOperatorWriteNode + assert_prism_eval("class Prism::TestCompilePrism; @@pit = 0; @@pit += 1; end") + end + + def test_ClassVariableOrWriteNode + assert_prism_eval("class Prism::TestCompilePrism; @@pit = 1; @@pit ||= 0; end") + assert_prism_eval("class Prism::TestCompilePrism; @@pit = nil; @@pit ||= 1; end") + end + + def test_ClassVariableWriteNode + assert_prism_eval("class Prism::TestCompilePrism; @@pit = 1; end") + end + + def test_ConstantAndWriteNode + assert_prism_eval("Constant = 1; Constant &&= 1") + end + + def test_ConstantOperatorWriteNode + assert_prism_eval("Constant = 1; Constant += 1") + end + + def test_ConstantOrWriteNode + assert_prism_eval("Constant = 1; Constant ||= 1") + end + + def test_ConstantWriteNode + # We don't call assert_prism_eval directly in this case because we + # don't want to assign the constant multiple times if we run + # with `--repeat-count` + # Instead, we eval manually here, and remove the constant to + constant_name = "YCT" + source = "#{constant_name} = 1" + prism_eval = RubyVM::InstructionSequence.compile_prism(source).eval + assert_equal prism_eval, 1 + Object.send(:remove_const, constant_name) + end + + def test_ConstantPathWriteNode + assert_prism_eval("Prism::CPWN = 1") + assert_prism_eval("::CPWN = 1") + end + + def test_ConstantPathAndWriteNode + assert_prism_eval("Prism::CPAWN = 1; Prism::CPAWN &&= 2") + assert_prism_eval("Prism::CPAWN &&= 1") + assert_prism_eval("::CPAWN = 1; ::CPAWN &&= 2") + end + + def test_ConstantPathOrWriteNode + assert_prism_eval("Prism::CPOrWN = nil; Prism::CPOrWN ||= 1") + assert_prism_eval("Prism::CPOrWN ||= 1") + assert_prism_eval("::CPOrWN = nil; ::CPOrWN ||= 1") + end + + def test_ConstantPathOperatorWriteNode + assert_prism_eval("Prism::CPOWN = 0; Prism::CPOWN += 1") + assert_prism_eval("::CPOWN = 0; ::CPOWN += 1") + end + + def test_GlobalVariableAndWriteNode + assert_prism_eval("$pit = 0; $pit &&= 1") + end + + def test_GlobalVariableOperatorWriteNode + assert_prism_eval("$pit = 0; $pit += 1") + end + + def test_GlobalVariableOrWriteNode + assert_prism_eval("$pit ||= 1") + end + + def test_GlobalVariableWriteNode + assert_prism_eval("$pit = 1") + end + + def test_InstanceVariableAndWriteNode + assert_prism_eval("@pit = 0; @pit &&= 1") + end + + def test_InstanceVariableOperatorWriteNode + assert_prism_eval("@pit = 0; @pit += 1") + end + + def test_InstanceVariableOrWriteNode + assert_prism_eval("@pit ||= 1") + end + + def test_InstanceVariableWriteNode + assert_prism_eval("class Prism::TestCompilePrism; @pit = 1; end") + end + + def test_LocalVariableAndWriteNode + assert_prism_eval("pit = 0; pit &&= 1") + end + + def test_LocalVariableOperatorWriteNode + assert_prism_eval("pit = 0; pit += 1") + end + + def test_LocalVariableOrWriteNode + assert_prism_eval("pit ||= 1") + end + + def test_LocalVariableWriteNode + assert_prism_eval("pit = 1") + assert_prism_eval(<<-CODE) + a = 0 + [].each do + a = 1 + end + a + CODE + + assert_prism_eval(<<-CODE) + a = 1 + d = 1 + [1].each do + b = 2 + a = 2 + [2].each do + c = 3 + d = 4 + a = 2 + end + end + [a, d] + CODE + end + + def test_MatchWriteNode + assert_prism_eval("/(?<foo>bar)(?<baz>bar>)/ =~ 'barbar'") + assert_prism_eval("/(?<foo>bar)/ =~ 'barbar'") + end + + ############################################################################ + # Multi-writes # + ############################################################################ + + def test_ClassVariableTargetNode + assert_prism_eval("class Prism::TestCompilePrism; @@pit, @@pit1 = 1; end") + end + + def test_ConstantTargetNode + # We don't call assert_prism_eval directly in this case because we + # don't want to assign the constant multiple times if we run + # with `--repeat-count` + # Instead, we eval manually here, and remove the constant to + constant_names = ["YCT", "YCT2"] + source = "#{constant_names.join(",")} = 1" + prism_eval = RubyVM::InstructionSequence.compile_prism(source).eval + assert_equal prism_eval, 1 + constant_names.map { |name| + Object.send(:remove_const, name) + } + end + + def test_ConstantPathTargetNode + assert_separately([], <<~'RUBY') + verbose = $VERBOSE + # Create some temporary nested constants + Object.send(:const_set, "MyFoo", Object) + Object.const_get("MyFoo").send(:const_set, "Bar", Object) + + constant_names = ["MyBar", "MyFoo::Bar", "MyFoo::Bar::Baz"] + source = "#{constant_names.join(",")} = Object" + iseq = RubyVM::InstructionSequence.compile_prism(source) + $VERBOSE = nil + prism_eval = iseq.eval + $VERBOSE = verbose + assert_equal prism_eval, Object + RUBY + end + + def test_GlobalVariableTargetNode + assert_prism_eval("$pit, $pit1 = 1") + end + + def test_InstanceVariableTargetNode + assert_prism_eval("class Prism::TestCompilePrism; @pit, @pit1 = 1; end") + end + + def test_LocalVariableTargetNode + assert_prism_eval("pit, pit1 = 1") + assert_prism_eval(<<-CODE) + a = 1 + [1].each do + c = 2 + a, b = 2 + end + a + CODE + end + + def test_MultiTargetNode + assert_prism_eval("a, (b, c) = [1, 2, 3]") + assert_prism_eval("a, (b, c) = [1, 2, 3]; a") + assert_prism_eval("a, (b, c) = [1, 2, 3]; b") + assert_prism_eval("a, (b, c) = [1, 2, 3]; c") + assert_prism_eval("a, (b, c) = [1, [2, 3]]; c") + assert_prism_eval("a, (b, *c) = [1, [2, 3]]; c") + assert_prism_eval("a, (b, *c) = 1, [2, 3]; c") + assert_prism_eval("a, (b, *) = 1, [2, 3]; b") + assert_prism_eval("a, (b, *c, d) = 1, [2, 3, 4]; [a, b, c, d]") + assert_prism_eval("(a, (b, c, d, e), f, g), h = [1, [2, 3]], 4, 5, [6, 7]; c") + end + + def test_MultiWriteNode + assert_prism_eval("foo, bar = [1, 2]") + assert_prism_eval("foo, = [1, 2]") + assert_prism_eval("foo, *, bar = [1, 2]") + assert_prism_eval("foo, bar = 1, 2") + assert_prism_eval("foo, *, bar = 1, 2") + assert_prism_eval("foo, *, bar = 1, 2, 3, 4") + assert_prism_eval("a, b, *, d = 1, 2, 3, 4") + assert_prism_eval("a, b, *, d = 1, 2") + assert_prism_eval("(a, b), *, c = [1, 3], 4, 5") + assert_prism_eval("(a, b), *, c = [1, 3], 4, 5; a") + assert_prism_eval("(a, b), *, c = [1, 3], 4, 5; b") + assert_prism_eval("(a, b), *, c = [1, 3], 4, 5; c") + assert_prism_eval("a, *, (c, d) = [1, 3], 4, 5; a") + assert_prism_eval("a, *, (c, d) = [1, 3], 4, 5; c") + assert_prism_eval("(a, b, c), *, (d, e) = [1, 3], 4, 5, [6, 7]") + assert_prism_eval("(a, b, c), *, (d, e) = [1, 3], 4, 5, [6, 7]; b") + assert_prism_eval("(a, b, c), *, (d, e) = [1, 3], 4, 5, [6, 7]; d") + assert_prism_eval("((a, *, b), *, (c, *, (d, *, e, f, g))), *, ((h, i, *, j), *, (k, l, m, *, n, o, p), q, r) = 1; a") + assert_prism_eval("*a = 1; a") + assert_prism_eval("_, {}[:foo] = 1") + assert_prism_eval("_, {}[:foo], _ = 1") + assert_prism_eval("_, {}[:foo], _ = 1") + assert_prism_eval("_,{}[:foo], _, {}[:bar] = 1") + assert_prism_eval("* = :foo") + assert_prism_eval("* = *[]") + assert_prism_eval("a, * = :foo") + + + assert_prism_eval(<<~CODE) + class Foo + def bar=(x); end + def baz=(c); end + end + foo = Foo.new + foo.bar, foo.baz = 1 + CODE + assert_prism_eval(<<~CODE) + class Foo + def bar=(x); end + def baz=(c); end + end + foo = Foo.new + _, foo.bar, foo.baz = 1 + CODE + assert_prism_eval(<<~CODE) + class Foo + def bar=(x); end + def baz=(c); end + end + foo = Foo.new + _, foo.bar, _, foo.baz = 1 + CODE + + # Test nested writes with method calls + assert_prism_eval(<<~RUBY) + class Foo + attr_accessor :bar + end + + a = Foo.new + + (a.bar, a.bar), b = [1], 2 + RUBY + assert_prism_eval(<<~RUBY) + h = {} + (h[:foo], h[:bar]), a = [1], 2 + RUBY + end + + ############################################################################ + # String-likes # + ############################################################################ + + def test_EmbeddedStatementsNode + assert_prism_eval('"foo #{to_s} baz"') + end + + def test_EmbeddedVariableNode + assert_prism_eval('class Prism::TestCompilePrism; @pit = 1; "#@pit"; end') + assert_prism_eval('class Prism::TestCompilePrism; @@pit = 1; "#@@pit"; end') + assert_prism_eval('$pit = 1; "#$pit"') + end + + def test_InterpolatedMatchLastLineNode + assert_prism_eval('$pit = ".oo"; if /"#{$pit}"/mix; end') + end + + def test_InterpolatedRegularExpressionNode + assert_prism_eval('$pit = 1; /1 #$pit 1/') + assert_prism_eval('$pit = 1; /#$pit/i') + assert_prism_eval('/1 #{1 + 2} 1/') + assert_prism_eval('/1 #{"2"} #{1 + 2} 1/') + end + + def test_InterpolatedStringNode + assert_prism_eval('$pit = 1; "1 #$pit 1"') + assert_prism_eval('"1 #{1 + 2} 1"') + assert_prism_eval('"Prism" "::" "TestCompilePrism"') + assert_prism_eval(<<-'RUBY') + # frozen_string_literal: true + + !("a""b""#{1}").frozen? + RUBY + assert_prism_eval(<<-'RUBY') + # frozen_string_literal: true + + !("a""#{1}""b").frozen? + RUBY + + # Test encoding of interpolated strings + assert_prism_eval(<<~'RUBY') + "#{"foo"}s".encoding + RUBY + assert_prism_eval(<<~'RUBY') + a = "foo" + b = "#{a}" << "Bar" + [a, b, b.encoding] + RUBY + end + + def test_concatenated_StringNode + assert_prism_eval('("a""b").frozen?') + assert_prism_eval(<<-CODE) + # frozen_string_literal: true + + ("a""b").frozen? + CODE + end + + def test_InterpolatedSymbolNode + assert_prism_eval('$pit = 1; :"1 #$pit 1"') + assert_prism_eval(':"1 #{1 + 2} 1"') + end + + def test_InterpolatedXStringNode + assert_prism_eval(<<~RUBY) + def self.`(command) = command * 2 + `echo \#{1}` + RUBY + + assert_prism_eval(<<~RUBY) + def self.`(command) = command * 2 + `echo \#{"100"}` + RUBY + end + + def test_MatchLastLineNode + assert_prism_eval("if /foo/; end") + assert_prism_eval("if /foo/i; end") + assert_prism_eval("if /foo/x; end") + assert_prism_eval("if /foo/m; end") + assert_prism_eval("if /foo/im; end") + assert_prism_eval("if /foo/mx; end") + assert_prism_eval("if /foo/xi; end") + assert_prism_eval("if /foo/ixm; end") + end + + def test_RegularExpressionNode + assert_prism_eval('/pit/') + assert_prism_eval('/pit/i') + assert_prism_eval('/pit/x') + assert_prism_eval('/pit/m') + assert_prism_eval('/pit/im') + assert_prism_eval('/pit/mx') + assert_prism_eval('/pit/xi') + assert_prism_eval('/pit/ixm') + + assert_prism_eval('/pit/u') + assert_prism_eval('/pit/e') + assert_prism_eval('/pit/s') + assert_prism_eval('/pit/n') + + assert_prism_eval('/pit/me') + assert_prism_eval('/pit/ne') + + assert_prism_eval('2.times.map { /#{1}/o }') + assert_prism_eval('2.times.map { foo = 1; /#{foo}/o }') + end + + def test_StringNode + assert_prism_eval('"pit"') + assert_prism_eval('"a".frozen?') + end + + def test_StringNode_frozen_string_literal_true + [ + # Test that string literal is frozen + <<~RUBY, + # frozen_string_literal: true + "a".frozen? + RUBY + # Test that two string literals with the same contents are the same string + <<~RUBY, + # frozen_string_literal: true + "hello".equal?("hello") + RUBY + ].each do |src| + assert_prism_eval(src, raw: true) + end + end + + def test_StringNode_frozen_string_literal_false + [ + # Test that string literal is frozen + <<~RUBY, + # frozen_string_literal: false + !"a".frozen? + RUBY + # Test that two string literals with the same contents are the same string + <<~RUBY, + # frozen_string_literal: false + !"hello".equal?("hello") + RUBY + ].each do |src| + assert_prism_eval(src, raw: true) + end + end + + def test_StringNode_frozen_string_literal_default + # Test that string literal is chilled + assert_prism_eval('"a".frozen?') + + # Test that two identical chilled string literals aren't the same object + assert_prism_eval('!"hello".equal?("hello")') + end + + def test_SymbolNode + assert_prism_eval(":pit") + + # Test UTF-8 symbol in a US-ASCII file + assert_prism_eval(<<~'RUBY', raw: true) + # -*- coding: us-ascii -*- + :"\u{e9}" + RUBY + + # Test ASCII-8BIT symbol in a US-ASCII file + assert_prism_eval(<<~'RUBY', raw: true) + # -*- coding: us-ascii -*- + :"\xff" + RUBY + + # Test US-ASCII symbol in a ASCII-8BIT file + assert_prism_eval(<<~'RUBY', raw: true) + # -*- coding: ascii-8bit -*- + :a + RUBY + end + + def test_XStringNode + assert_prism_eval(<<~RUBY) + class Prism::TestCompilePrism + def self.`(command) = command * 2 + `pit` + end + RUBY + end + + ############################################################################ + # Structures # + ############################################################################ + + def test_ArrayNode + assert_prism_eval("[]") + assert_prism_eval("[1, 2, 3]") + assert_prism_eval("%i[foo bar baz]") + assert_prism_eval("%w[foo bar baz]") + assert_prism_eval("[*1..2]") + assert_prism_eval("[*1..2, 3, 4, *5..6, 7, 8]") + assert_prism_eval("[*1..2, 3, 4, *5..6, 7, 8, *9..11]") + assert_prism_eval("[0, *1..2, 3, 4, *5..6, 7, 8, *9..11]") + assert_prism_eval("[-1, true, 0, *1..2, 3, 4, *5..6, 7, 8, *9..11]") + assert_prism_eval("a = [1,2]; [0, *a, 3, 4, *5..6, 7, 8, *9..11]") + assert_prism_eval("[[*1..2], 3, *4..5]") + + elements = Array.new(64) { ":foo" } + assert_prism_eval("[#{elements.join(", ")}, bar: 1, baz: 2]") + + # Test keyword splat inside of array + assert_prism_eval("[**{x: 'hello'}]") + + # Test UTF-8 string array literal in a US-ASCII file + assert_prism_eval(<<~'RUBY', raw: true) + # -*- coding: us-ascii -*- + # frozen_string_literal: true + %W"\u{1f44b} \u{1f409}" + RUBY + end + + def test_AssocNode + assert_prism_eval("{ foo: :bar }") + end + + def test_AssocSplatNode + assert_prism_eval("foo = { a: 1 }; { **foo }") + assert_prism_eval("foo = { a: 1 }; bar = foo; { **foo, b: 2, **bar, c: 3 }") + assert_prism_eval("foo = { a: 1 }; { b: 2, **foo, c: 3}") + + # Test anonymous AssocSplatNode + assert_prism_eval(<<~RUBY) + o = Object.new + def o.bar(**) = Hash(**) + + o.bar(hello: "world") + RUBY + + # Test that AssocSplatNode is evaluated before BlockArgumentNode using + # the splatkw instruction + assert_prism_eval(<<~RUBY) + o = Struct.new(:ary) do + def to_hash + ary << :to_hash + {} + end + + def to_proc + ary << :to_proc + -> {} + end + + def t(...); end + end.new + o.ary = [] + + o.t(**o, &o) + o.ary + RUBY + end + + def test_HashNode + assert_prism_eval("{}") + assert_prism_eval("{ a: :a }") + assert_prism_eval("{ a: :a, b: :b }") + assert_prism_eval("a = 1; { a: a }") + assert_prism_eval("a = 1; { a: }") + assert_prism_eval("{ to_s: }") + assert_prism_eval("{ Prism: }") + assert_prism_eval("[ Prism: [:b, :c]]") + assert_prism_eval("{ [] => 1}") + end + + def test_ImplicitNode + assert_prism_eval("{ to_s: }") + end + + def test_RangeNode + assert_prism_eval("1..2") + assert_prism_eval("1...2") + assert_prism_eval("..2") + assert_prism_eval("...2") + assert_prism_eval("1..") + assert_prism_eval("1...") + assert_prism_eval("a1 = 1; a2 = 2; a1..a2") + assert_prism_eval("a1 = 1; a2 = 2; a1...a2") + assert_prism_eval("a2 = 2; ..a2") + assert_prism_eval("a2 = 2; ...a2") + assert_prism_eval("a1 = 1; a1..") + assert_prism_eval("a1 = 1; a1...") + assert_prism_eval("1..2; nil") + assert_prism_eval("1...2; nil") + assert_prism_eval("..2; nil") + assert_prism_eval("...2; nil") + assert_prism_eval("1..; nil") + assert_prism_eval("1...; nil") + assert_prism_eval("a1 = 1; a2 = 2; a1..a2; nil") + assert_prism_eval("a1 = 1; a2 = 2; a1...a2; nil") + assert_prism_eval("a2 = 2; ..a2; nil") + assert_prism_eval("a2 = 2; ...a2; nil") + assert_prism_eval("a1 = 1; a1..; nil") + assert_prism_eval("a1 = 1; a1...; nil") + end + + def test_SplatNode + assert_prism_eval("*b = []; b") + assert_prism_eval("*b = [1, 2, 3]; b") + assert_prism_eval("a, *b = [1, 2, 3]; a") + assert_prism_eval("a, *b = [1, 2, 3]; b") + assert_prism_eval("a, *b, c = [1, 2, 3]; a") + assert_prism_eval("a, *b, c = [1, 2, 3]; b") + assert_prism_eval("a, *b, c = [1, 2, 3]; c") + assert_prism_eval("*b, c = [1, 2, 3]; b") + assert_prism_eval("*b, c = [1, 2, 3]; c") + assert_prism_eval("a, *, c = [1, 2, 3]; a") + assert_prism_eval("a, *, c = [1, 2, 3]; c") + + # Test anonymous splat node + assert_prism_eval(<<~RUBY) + def self.bar(*) = Array(*) + + bar([1, 2, 3]) + RUBY + end + + ############################################################################ + # Jumps # + ############################################################################ + + def test_AndNode + assert_prism_eval("true && 1") + assert_prism_eval("false && 1") + end + + def test_CaseNode + assert_prism_eval("case :a; when :a; 1; else; 2; end") + assert_prism_eval("case :a; when :b; 1; else; 2; end") + assert_prism_eval("case :a; when :a; 1; else; 2; end") + assert_prism_eval("case :a; when :a; end") + assert_prism_eval("case :a; when :b, :c; end") + assert_prism_eval("case; when :a; end") + assert_prism_eval("case; when :a, :b; 1; else; 2 end") + assert_prism_eval("case :a; when :b; else; end") + assert_prism_eval("b = 1; case :a; when b; else; end") + assert_prism_eval(<<-CODE) + def self.prism_test_case_node + case :a + when :b + else + return 2 + end + 1 + end + prism_test_case_node + CODE + + # Test splat in when + assert_prism_eval(<<~RUBY) + ary = [1, 2] + case 1 + when *ary + :ok + else + :ng + end + RUBY + + # Test splat in when + assert_prism_eval(<<~RUBY) + ary = [1, 2] + case 1 + when :foo, *ary + :ok + else + :ng + end + RUBY + + # Test case without predicate + assert_prism_eval(<<~RUBY) + case + when 1 == 2 + :ng + else + :ok + end + RUBY + + # test splat with no predicate + assert_prism_eval(<<~RUBY) + case + when *[true] + :ok + else + :ng + end + RUBY + end + + def test_ElseNode + assert_prism_eval("if false; 0; else; 1; end") + assert_prism_eval("if true; 0; else; 1; end") + assert_prism_eval("true ? 1 : 0") + assert_prism_eval("false ? 0 : 1") + end + + def test_FlipFlopNode + assert_prism_eval("not (1 == 1) .. (2 == 2)") + assert_prism_eval("not (1 == 1) ... (2 == 2)") + end + + def test_IfNode + assert_prism_eval("if true; 1; end") + assert_prism_eval("1 if true") + assert_prism_eval('a = b = 1; if a..b; end') + assert_prism_eval('if "a".."b"; end') + assert_prism_eval('if "a"..; end') + assert_prism_eval('if .."b"; end') + assert_prism_eval('if ..1; end') + assert_prism_eval('if 1..; end') + assert_prism_eval('if 1..2; end') + assert_prism_eval('if true or true; end'); + end + + def test_OrNode + assert_prism_eval("true || 1") + assert_prism_eval("false || 1") + end + + def test_UnlessNode + assert_prism_eval("1 unless true") + assert_prism_eval("1 unless false") + assert_prism_eval("unless true; 1; end") + assert_prism_eval("unless false; 1; end") + end + + def test_UntilNode + assert_prism_eval("a = 0; until a == 1; a = a + 1; end") + + # Test UntilNode in rescue + assert_prism_eval(<<~RUBY) + o = Object.new + o.instance_variable_set(:@ret, []) + def o.foo = @ret << @ret.length + def o.bar = @ret.length > 3 + begin + raise + rescue + o.foo until o.bar + end + o.instance_variable_get(:@ret) + RUBY + end + + def test_WhileNode + assert_prism_eval("a = 0; while a != 1; a = a + 1; end") + + # Test WhileNode in rescue + assert_prism_eval(<<~RUBY) + o = Object.new + o.instance_variable_set(:@ret, []) + def o.foo = @ret << @ret.length + def o.bar = @ret.length < 3 + begin + raise + rescue + o.foo while o.bar + end + o.instance_variable_get(:@ret) + RUBY + end + + def test_ForNode + 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("r = []; for foo, in [1,2,3] do r << foo end; r") + + 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 + + ############################################################################ + # Throws # + ############################################################################ + + def test_BeginNode + assert_prism_eval("begin; 1; end") + assert_prism_eval("begin; end; 1") + end + + def test_BreakNode + assert_prism_eval("while true; break; end") + assert_prism_eval("while true; break 1; end") + assert_prism_eval("while true; break 1, 2; end") + + assert_prism_eval("[].each { break }") + assert_prism_eval("[true].map { break }") + end + + def test_ensure_in_methods + assert_prism_eval(<<-CODE) +def self.m + a = [] +ensure + a << 5 + return a +end +m + CODE + end + + def test_break_runs_ensure + assert_prism_eval(<<-CODE) +a = [] +while true + begin + break + ensure + a << 1 + end +end +a + CODE + end + + def test_EnsureNode + assert_prism_eval("begin; 1; ensure; 2; end") + assert_prism_eval("begin; 1; begin; 3; ensure; 4; end; ensure; 2; end") + assert_prism_eval(<<-CODE) + begin + a = 2 + ensure + end + CODE + assert_prism_eval(<<-CODE) + begin + a = 2 + ensure + a = 3 + end + a + CODE + + # Test that ensure block only evaluated once + assert_prism_eval(<<~RUBY) + res = [] + begin + begin + raise + ensure + res << $!.to_s + end + rescue + res + end + RUBY + + assert_prism_eval(<<-CODE) + a = 1 + begin + a = 2 + ensure + a = 3 + end + a + CODE + assert_prism_eval(<<-CODE) + a = 1 + begin + b = 2 + ensure + c = 3 + end + a + b + c + CODE + assert_prism_eval(<<~CODE) + foo = 1 + begin + ensure + begin + ensure + foo.nil? + end + end + CODE + assert_prism_eval(<<~CODE) + def test + ensure + {}.each do |key, value| + {}[key] = value + end + end + CODE + assert_prism_eval(<<~CODE) + def test + a = 1 + ensure + {}.each do |key, value| + {}[key] = a + end + end + CODE + assert_prism_eval(<<-CODE) + def self.prism_test_ensure_node + begin + ensure + end + return + end + prism_test_ensure_node + CODE + + # Test empty ensure block + assert_prism_eval(<<~RUBY) + res = [] + + begin + begin + raise + ensure + end + rescue + res << "rescue" + end + + res + RUBY + + # Bug #21001 + assert_prism_eval(<<~RUBY) + RUN_ARRAY = [1,2] + + MAP_PROC = Proc.new do |&blk| + block_results = [] + RUN_ARRAY.each do |value| + block_value = blk.call(value) + block_results.push block_value + end + block_results + ensure + next block_results + end + + MAP_PROC.call do |value| + break if value > 1 + next value + end + RUBY + end + + def test_NextNode + assert_prism_eval("2.times do |i|; next if i == 1; end") + + assert_prism_eval(<<-CODE) + res = [] + i = 0 + while i < 5 + i += 1 + next if i == 3 + res << i + end + res + CODE + + assert_prism_eval(<<-CODE) + res = [] + (1..5).each do |i| + next if i.even? + res << i + end + res + CODE + + assert_prism_eval(<<-CODE) + (1..5).map do |i| + next i, :even if i.even? + i + end + CODE + + assert_prism_eval(<<-CODE) + res = [] + i = 0 + begin + i += 1 + next if i == 3 + res << i + end while i < 5 + res + CODE + + assert_prism_eval(<<-CODE) + while false + begin + ensure + end + next + end + CODE + + assert_prism_eval(<<~CODE) + [].each do + begin + rescue + next + end + end + CODE + end + + def test_RedoNode + assert_prism_eval(<<-CODE) + counter = 0 + + 5.times do |i| + counter += 1 + if i == 2 && counter < 3 + redo + end + end + CODE + + assert_prism_eval(<<-CODE) + for i in 1..5 + if i == 3 + i = 0 + redo + end + end + CODE + + assert_prism_eval(<<-CODE) + i = 0 + begin + i += 1 + redo if i == 3 + end while i < 5 + CODE + end + + def test_RescueNode + assert_prism_eval("begin; 1; rescue; 2; end") + assert_prism_eval(<<~CODE) + begin + 1 + rescue SyntaxError + 2 + end + CODE + assert_prism_eval(<<~CODE) + begin + 1 + raise 'boom' + rescue StandardError + 2 + end + CODE + assert_prism_eval(<<~CODE) + begin + a = 1 + rescue StandardError => e + end + CODE + assert_prism_eval(<<~CODE) + begin + raise StandardError + rescue StandardError => e + end + CODE + assert_prism_eval(<<~CODE) + begin + 1 + rescue StandardError => e + e + rescue SyntaxError => f + f + else + 4 + end + CODE + assert_prism_eval(<<-CODE) + begin + a = 2 + rescue + a = 3 + end + a + CODE + assert_prism_eval(<<-CODE) + a = 1 + begin + a = 2 + rescue + a = 3 + end + a + CODE + assert_prism_eval(<<-CODE) + a = 1 + begin + b = 2 + raise "bang" + rescue + c = 3 + end + a + b + c + CODE + assert_prism_eval("begin; rescue; end") + + assert_prism_eval(<<~CODE) + begin + rescue + args.each do |key, value| + tmp[key] = 1 + end + end + CODE + assert_prism_eval(<<~CODE) + 10.times do + begin + rescue + break + end + end + CODE + + # Test RescueNode with ElseNode + assert_prism_eval(<<~RUBY) + calls = [] + begin + begin + rescue RuntimeError + calls << 1 + else + calls << 2 + raise RuntimeError + end + rescue RuntimeError + end + + calls + RUBY + end + + def test_RescueModifierNode + assert_prism_eval("1.nil? rescue false") + assert_prism_eval("1.nil? rescue 1") + assert_prism_eval("raise 'bang' rescue nil") + assert_prism_eval("raise 'bang' rescue a = 1; a.nil?") + assert_prism_eval("a = 0 rescue (a += 1 && retry if a <= 1)") + end + + def test_RetryNode + assert_prism_eval(<<~CODE) + a = 1 + begin + a + raise "boom" + rescue + a += 1 + retry unless a > 1 + ensure + a = 3 + end + CODE + + assert_prism_eval(<<~CODE) + begin + rescue + foo = 2 + retry + end + CODE + + assert_prism_eval(<<~CODE) + begin + a = 2 + rescue + retry + end + CODE + end + + def test_ReturnNode + assert_prism_eval(<<-CODE) + def self.prism_test_return_node + return 1 + end + prism_test_return_node + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_return_node + return 1, 2 + end + prism_test_return_node + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_return_node + [1].each do |e| + return true + end + end + prism_test_return_node + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_return_node + [1].map do |i| + return i if i == 1 + 2 + end + end + prism_test_return_node + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_return_node(*args, **kwargs) + return *args, *args, **kwargs + end + prism_test_return_node(1, foo: 0) + CODE + end + + ############################################################################ + # Scopes/statements # + ############################################################################ + + def test_BlockNode + assert_prism_eval("[1, 2, 3].each { |num| num }") + + assert_prism_eval("[].tap { _1 }") + + assert_prism_eval("[].each { |a,| }") + assert_prism_eval("[[1, 2, 3]].map { |_, _, a| a }") + assert_prism_eval("[[1, 2, 3]].map { |_, a| a }") + + assert_prism_eval("[[]].map { |a| a }") + assert_prism_eval("[[]].map { |a| a }") + assert_prism_eval("[[]].map { |a, &block| a }") + assert_prism_eval("[[]].map { |a, &block| a }") + assert_prism_eval("[{}].map { |a,| }") + assert_prism_eval("[[]].map { |a,b=1| a }") + assert_prism_eval("[{}].map { |a,| }") + assert_prism_eval("[{}].map { |a| a }") + + # Test blocks with MultiTargetNode + assert_prism_eval("[[1, 2]].each.map { |(a), (b)| [a, b] }") + end + + def test_ClassNode + assert_prism_eval("class PrismClassA; end") + assert_prism_eval("class PrismClassA; end; class PrismClassB < PrismClassA; end") + assert_prism_eval("class PrismClassA; end; class PrismClassA::PrismClassC; end") + assert_prism_eval(<<-HERE + class PrismClassA; end + class PrismClassA::PrismClassC; end + class PrismClassB; end + class PrismClassB::PrismClassD < PrismClassA::PrismClassC; end + HERE + ) + end + + # Many of these tests are versions of tests at bootstraptest/test_method.rb + def test_DefNode + assert_prism_eval("def prism_test_def_node; end") + assert_prism_eval("a = Object.new; def a.prism_singleton; :ok; end; a.prism_singleton") + assert_prism_eval("def self.prism_test_def_node() 1 end; prism_test_def_node()") + assert_prism_eval("def self.prism_test_def_node(a,b) [a, b] end; prism_test_def_node(1,2)") + assert_prism_eval("def self.prism_test_def_node(a,x=7,y=1) x end; prism_test_def_node(7,1)") + assert_prism_eval("def self.prism_test_def_node(a = 1); x = 2; end; prism_test_def_node") + + # rest argument + assert_prism_eval("def self.prism_test_def_node(*a) a end; prism_test_def_node().inspect") + assert_prism_eval("def self.prism_test_def_node(*a) a end; prism_test_def_node(1).inspect") + assert_prism_eval("def self.prism_test_def_node(x,y,*a) a end; prism_test_def_node(7,7,1,2).inspect") + assert_prism_eval("def self.prism_test_def_node(x,y=7,*a) a end; prism_test_def_node(7).inspect") + assert_prism_eval("def self.prism_test_def_node(x,y,z=7,*a) a end; prism_test_def_node(7,7).inspect") + assert_prism_eval("def self.prism_test_def_node(x,y,z=7,zz=7,*a) a end; prism_test_def_node(7,7,7).inspect") + + # keyword arguments + assert_prism_eval("def self.prism_test_def_node(a: 1, b: 2, c: 4) a + b + c; end; prism_test_def_node(a: 2)") + assert_prism_eval("def self.prism_test_def_node(a: 1, b: 2, c: 4) a + b + c; end; prism_test_def_node(b: 3)") + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(x = 1, y, a: 8, b: 2, c: 4) + a + b + c + x + y + end + prism_test_def_node(10, b: 3) + CODE + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(a: []) + a + end + prism_test_def_node + CODE + + # block arguments + assert_prism_eval("def self.prism_test_def_node(&block) block end; prism_test_def_node{}.class") + assert_prism_eval("def self.prism_test_def_node(&block) block end; prism_test_def_node().inspect") + assert_prism_eval("def self.prism_test_def_node(a,b=7,*c,&block) b end; prism_test_def_node(7,1).inspect") + assert_prism_eval("def self.prism_test_def_node(a,b=7,*c,&block) c end; prism_test_def_node(7,7,1).inspect") + + # splat + assert_prism_eval("def self.prism_test_def_node(a) a end; prism_test_def_node(*[1])") + assert_prism_eval("def self.prism_test_def_node(x,a) a end; prism_test_def_node(7,*[1])") + assert_prism_eval("def self.prism_test_def_node(x,y,a) a end; prism_test_def_node(7,7,*[1])") + assert_prism_eval("def self.prism_test_def_node(x,y,a,b,c) a end; prism_test_def_node(7,7,*[1,7,7])") + + # recursive call + assert_prism_eval("def self.prism_test_def_node(n) n == 0 ? 1 : prism_test_def_node(n-1) end; prism_test_def_node(5)") + + # instance method + assert_prism_eval("class PrismTestDefNode; def prism_test_def_node() 1 end end; PrismTestDefNode.new.prism_test_def_node") + assert_prism_eval("class PrismTestDefNode; def prism_test_def_node(*a) a end end; PrismTestDefNode.new.prism_test_def_node(1).inspect") + + # block argument + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(&block) prism_test_def_node2(&block) end + def self.prism_test_def_node2() yield 1 end + prism_test_def_node2 {|a| a } + CODE + + # multi argument + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(a, (b, *c, d)) + [a, b, c, d] + end + prism_test_def_node("a", ["b", "c", "d"]) + CODE + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(a, (b, c, *)) + [a, b, c] + end + prism_test_def_node("a", ["b", "c"]) + CODE + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(a, (*, b, c)) + [a, b, c] + end + prism_test_def_node("a", ["b", "c"]) + CODE + + # recursive multis + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(a, (b, *c, (d, *e, f))) + [a, b, c, d, d, e, f] + end + prism_test_def_node("a", ["b", "c", ["d", "e", "f"]]) + CODE + + # Many arguments + assert_prism_eval(<<-CODE) + def self.prism_test_def_node(a, (b, *c, d), e = 1, *f, g, (h, *i, j), k:, l: 1, **m) + [a, b, c, d, e, f, g, h, i, j, k, l, m] + end + prism_test_def_node( + "a", + ["b", "c1", "c2", "d"], + "e", + "f1", "f2", + "g", + ["h", "i1", "i2", "j"], + k: "k", + l: "l", + m1: "m1", + m2: "m2" + ) + CODE + end + + def test_pow_parameters + assert_prism_eval("def self.m(a, **); end; method(:m).parameters") + end + + def test_star_parameters + assert_prism_eval("def self.m(a, *, b); end; method(:m).parameters") + end + + def test_repeated_block_params + assert_prism_eval("def self.x(&blk); blk; end; x { |_, _, _ = 1, *_, _:, _: 2, **_, &_| }.parameters") + end + + def test_repeated_proc_params + assert_prism_eval("proc {|_, _, _ = 1, *_, _:, _: 2, **_, &_| }.parameters") + end + + def test_forward_parameters_block + assert_prism_eval("def self.m(&); end; method(:m).parameters") + end + + def test_forward_parameters + assert_prism_eval("def self.m(...); end; method(:m).parameters") + end + + def test_repeated_block_underscore + assert_prism_eval("def self.m(_, **_, &_); _; end; method(:m).parameters") + end + + def test_repeated_kw_rest_underscore + assert_prism_eval("def self.m(_, **_); _; end; method(:m).parameters") + end + + def test_repeated_required_keyword_underscore + assert_prism_eval("def self.m(_, _, *_, _, _:); _; end; method(:m).parameters") + assert_prism_eval("def self.m(_, _, *_, _, _:, _: 2); _; end; method(:m).parameters") + end + + def test_repeated_required_post_underscore + assert_prism_eval("def self.m(_, _, *_, _); _; end; method(:m).parameters") + end + + def test_repeated_splat_underscore + assert_prism_eval("def self.m(_, _, _ = 1, _ = 2, *_); end; method(:m).parameters") + end + + def test_repeated_optional_underscore + assert_prism_eval("def self.m(a, _, _, _ = 1, _ = 2, b); end; method(:m).parameters") + end + + def test_repeated_required_underscore + assert_prism_eval("def self.m(a, _, _, b); end; method(:m).parameters") + end + + def test_locals_in_parameters + assert_prism_eval("def self.m(a = b = c = 1); [a, b, c]; end; self.m") + end + + def test_trailing_comma_on_block + assert_prism_eval("def self.m; yield [:ok]; end; m {|v0,| v0 }") + end + + def test_complex_default_params + assert_prism_eval("def self.foo(a:, b: '2'.to_i); [a, b]; end; foo(a: 1)") + assert_prism_eval("def self.foo(a:, b: 2, c: '3'.to_i); [a, b, c]; end; foo(a: 1)") + end + + def test_numbered_params + assert_prism_eval("[1, 2, 3].then { _3 }") + assert_prism_eval("1.then { one = 1; one + _1 }") + end + + def test_rescue_with_ensure + assert_prism_eval(<<-CODE) +begin + begin + raise "a" + rescue + raise "b" + ensure + raise "c" + end +rescue => e + e.message +end + CODE + end + + def test_required_kwarg_ordering + assert_prism_eval("def self.foo(a: 1, b:); [a, b]; end; foo(b: 2)") + end + + def test_trailing_keyword_method_params + # foo(1, b: 2, c: 3) # argc -> 3 + assert_prism_eval("def self.foo(a, b:, c:); [a, b, c]; end; foo(1, b: 2, c: 3)") + end + + def test_keyword_method_params_only + # foo(a: 1, b: 2) # argc -> 2 + assert_prism_eval("def self.foo(a:, b:); [a, b]; end; foo(a: 1, b: 2)") + end + + def test_keyword_method_params_with_splat + # foo(a: 1, **b) # argc -> 1 + assert_prism_eval("def self.foo(a:, b:); [a, b]; end; b = { b: 2 }; foo(a: 1, **b)") + end + + def test_positional_and_splat_keyword_method_params + # foo(a, **b) # argc -> 2 + assert_prism_eval("def self.foo(a, b); [a, b]; end; b = { b: 2 }; foo(1, **b)") + end + + def test_positional_and_splat_method_params + # foo(a, *b, c, *d, e) # argc -> 2 + assert_prism_eval("def self.foo(a, b, c, d, e); [a, b, c, d, e]; end; b = [2]; d = [4]; foo(1, *b, 3, *d, 5)") + end + + def test_positional_with_splat_and_splat_keyword_method_params + # foo(a, *b, c, *d, **e) # argc -> 3 + assert_prism_eval("def self.foo(a, b, c, d, e); [a, b, c, d, e]; end; b = [2]; d = [4]; e = { e: 5 }; foo(1, *b, 3, *d, **e)") + end + + def test_positional_with_splat_and_keyword_method_params + # foo(a, *b, c, *d, e:) # argc -> 3 + assert_prism_eval("def self.foo(a, b, c, d, e:); [a, b, c, d, e]; end; b = [2]; d = [4]; foo(1, *b, 3, *d, e: 5)") + end + + def test_leading_splat_and_keyword_method_params + # foo(*a, b:) # argc -> 2 + assert_prism_eval("def self.foo(a, b:); [a, b]; end; a = [1]; foo(*a, b: 2)") + end + + def test_repeated_method_params + assert_prism_eval("def self.foo(_a, _a); _a; end; foo(1, 2)") + end + + def test_splat_params_with_no_lefties + assert_prism_eval("def self.foo(v, (*)); v; end; foo(1, [2, 3, 4])") + end + + def test_method_parameters + assert_prism_eval(<<-CODE) + def self.prism_test_method_parameters(a, b=1, *c, d:, e: 2, **f, &g) + end + + method(:prism_test_method_parameters).parameters + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_method_parameters(d:, e: 2, **f, &g) + end + + method(:prism_test_method_parameters).parameters + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_method_parameters(**f, &g) + end + + method(:prism_test_method_parameters).parameters + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_method_parameters(&g) + end + + method(:prism_test_method_parameters).parameters + CODE + end + + def test_LambdaNode + assert_prism_eval("-> { to_s }.call") + end + + def test_LambdaNode_with_multiline_args + assert_prism_eval(<<-CODE) + -> (a, + b) { + a + b + }.call(1, 2) + CODE + end + + def test_ModuleNode + assert_prism_eval("module M; end") + assert_prism_eval("module M::N; end") + assert_prism_eval("module ::O; end") + end + + def test_ParenthesesNode + assert_prism_eval("()") + assert_prism_eval("(1)") + end + + def test_PreExecutionNode + assert_prism_eval("BEGIN { a = 1 }; 2", raw: true) + assert_prism_eval("b = 2; BEGIN { a = 1 }; a + b", raw: true) + end + + def test_PostExecutionNode + assert_prism_eval("END { 1 }") + assert_prism_eval("END { @b }; @b = 1") + assert_prism_eval("END { @b; 0 }; @b = 1") + assert_prism_eval("foo = 1; END { foo.nil? }") + assert_prism_eval("foo = 1; END { END { foo.nil? }}") + end + + def test_ProgramNode + assert_prism_eval("") + assert_prism_eval("1") + end + + def test_SingletonClassNode + assert_prism_eval("class << self; end") + end + + def test_StatementsNode + assert_prism_eval("1") + end + + def test_YieldNode + assert_prism_eval("def prism_test_yield_node; yield; end") + assert_prism_eval("def prism_test_yield_node; yield 1, 2; end") + assert_prism_eval("def prism_test_yield_node; yield **kw if condition; end") + + # Test case where there's a call directly after the yield call + assert_prism_eval("def prism_test_yield_node; yield; 1; end") + assert_prism_eval("def prism_test_yield_node; yield 1, 2; 1; end") + end + + ############################################################################ + # Calls / arguments # + ############################################################################ + + def test_ArgumentsNode + # assert_prism_eval("[].push 1") + end + + def test_BlockArgumentNode + assert_prism_eval("1.then(&:to_s)") + + # Test anonymous block forwarding + assert_prism_eval(<<~RUBY) + o = Object.new + def o.foo(&) = yield + def o.bar(&) = foo(&) + + o.bar { :ok } + RUBY + end + + def test_BlockLocalVariableNode + assert_prism_eval(<<-CODE + pm_var = "outer scope variable" + + 1.times { |;pm_var| pm_var = "inner scope variable"; pm_var } + CODE + ) + + assert_prism_eval(<<-CODE + pm_var = "outer scope variable" + + 1.times { |;pm_var| pm_var = "inner scope variable"; pm_var } + pm_var + CODE + ) + end + + def test_CallNode + assert_prism_eval("to_s") + + # with arguments + assert_prism_eval("eval '1'") + + # with arguments and popped + assert_prism_eval("eval '1'; 1") + + # With different types of calling arguments + assert_prism_eval(<<-CODE) + def self.prism_test_call_node_double_splat(**); end + prism_test_call_node_double_splat(b: 1, **{}) + CODE + assert_prism_eval(<<-CODE) + prism_test_call_node_double_splat(:b => 1) + CODE + + assert_prism_eval(<<-CODE) + def self.prism_test_call_node_splat(*); end + prism_test_call_node_splat(*[], 1) + CODE + + assert_prism_eval("prism_test_call_node_splat(*[], 1, 2)") + + assert_prism_eval(<<~RUBY) + def self.prism_test_call_node_splat_and_double_splat(a, b, **opts); end + prism_test_call_node_splat_and_double_splat(*[1], 2, **{}) + RUBY + + assert_prism_eval(<<-CODE) + class Foo + def []=(a, b) + 1234 + end + end + + def self.foo(i, j) + tbl = Foo.new + tbl[i] = j + end + foo(1, 2) + CODE + + assert_prism_eval(<<-CODE) + class Foo + def i=(a) + 1234 + end + end + + def self.foo(j) + tbl = Foo.new + tbl.i = j + end + foo(1) + CODE + + assert_prism_eval(<<-CODE) + foo = Object.new + def foo.[]=(k,v); 42; end + foo.[]=(1,2) + CODE + + # With splat inside of []= + assert_prism_eval(<<~RUBY) + obj = Object.new + def obj.[]=(a, b); 10; end + obj[*[1]] = 3 + RUBY + + assert_prism_eval(<<-CODE) + def self.prism_opt_var_trail_hash(a = nil, *b, c, **d); end + prism_opt_var_trail_hash("a") + prism_opt_var_trail_hash("a", c: 1) + prism_opt_var_trail_hash("a", "b") + prism_opt_var_trail_hash("a", "b", "c") + prism_opt_var_trail_hash("a", "b", "c", c: 1) + prism_opt_var_trail_hash("a", "b", "c", "c" => 0, c: 1) + CODE + + assert_prism_eval(<<-CODE) + def self.foo(*args, **kwargs) = [args, kwargs] + + [ + foo(2 => 3), + foo([] => 42), + foo(a: 42, b: 61), + foo(1, 2, 3, a: 42, "b" => 61), + foo(:a => 42, :b => 61), + ] + CODE + + assert_prism_eval(<<-CODE) + class PrivateMethod + def initialize + self.instance_var + end + private + attr_accessor :instance_var + end + pm = PrivateMethod.new + pm.send(:instance_var) + CODE + + # Testing safe navigation operator + assert_prism_eval(<<-CODE) + def self.test_prism_call_node + if [][0]&.first + 1 + end + end + test_prism_call_node + CODE + + # Specialized instructions + assert_prism_eval(%{-"literal"}) + assert_prism_eval(%{"literal".freeze}) + end + + def test_CallAndWriteNode + assert_prism_eval(<<-CODE + class PrismTestSubclass; end + def PrismTestSubclass.test_call_and_write_node; end; + PrismTestSubclass.test_call_and_write_node &&= 1 + CODE + ) + + assert_prism_eval(<<-CODE + def PrismTestSubclass.test_call_and_write_node + "str" + end + def PrismTestSubclass.test_call_and_write_node=(val) + val + end + PrismTestSubclass.test_call_and_write_node &&= 1 + CODE + ) + + assert_prism_eval(<<-CODE + def self.test_call_and_write_node; end; + self.test_call_and_write_node &&= 1 + CODE + ) + + assert_prism_eval(<<-CODE + def self.test_call_and_write_node + "str" + end + def self.test_call_and_write_node=(val) + val + end + self.test_call_and_write_node &&= 1 + CODE + ) + + assert_prism_eval(<<-CODE) + def self.test_prism_call_node; end + def self.test_prism_call_node=(val) + val + end + self&.test_prism_call_node &&= 1 + CODE + + assert_prism_eval(<<-CODE) + def self.test_prism_call_node + 2 + end + def self.test_prism_call_node=(val) + val + end + self&.test_prism_call_node &&= 1 + CODE + end + + def test_CallOrWriteNode + assert_prism_eval(<<-CODE + class PrismTestSubclass; end + def PrismTestSubclass.test_call_or_write_node; end; + def PrismTestSubclass.test_call_or_write_node=(val) + val + end + PrismTestSubclass.test_call_or_write_node ||= 1 + CODE + ) + + assert_prism_eval(<<-CODE + def PrismTestSubclass.test_call_or_write_node + "str" + end + PrismTestSubclass.test_call_or_write_node ||= 1 + CODE + ) + + assert_prism_eval(<<-CODE + def self.test_call_or_write_node; end; + def self.test_call_or_write_node=(val) + val + end + self.test_call_or_write_node ||= 1 + CODE + ) + + assert_prism_eval(<<-CODE + def self.test_call_or_write_node + "str" + end + self.test_call_or_write_node ||= 1 + CODE + ) + + assert_prism_eval(<<-CODE) + def self.test_prism_call_node + 2 + end + def self.test_prism_call_node=(val) + val + end + self&.test_prism_call_node ||= 1 + CODE + + assert_prism_eval(<<-CODE) + def self.test_prism_call_node; end + def self.test_prism_call_node=(val) + val + end + self&.test_prism_call_node ||= 1 + CODE + end + + def test_CallOperatorWriteNode + assert_prism_eval(<<-CODE + class PrismTestSubclass; end + def PrismTestSubclass.test_call_operator_write_node + 2 + end + def PrismTestSubclass.test_call_operator_write_node=(val) + val + end + PrismTestSubclass.test_call_operator_write_node += 1 + CODE + ) + end + + def test_ForwardingArgumentsNode + assert_prism_eval(<<-CODE) + def prism_test_forwarding_arguments_node(...); end; + def prism_test_forwarding_arguments_node1(...) + prism_test_forwarding_arguments_node(...) + end + CODE + + assert_prism_eval(<<-CODE) + def prism_test_forwarding_arguments_node(...); end; + def prism_test_forwarding_arguments_node1(a, ...) + prism_test_forwarding_arguments_node(1,2, 3, ...) + end + CODE + + assert_prism_eval(<<~RUBY) + o = Object.new + def o.bar(a, b, c) = [a, b, c] + def o.foo(...) = 1.times { bar(...) } + + o.foo(1, 2, 3) + 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") + assert_prism_eval(<<-CODE) + class A + def initialize(a, b) + end + end + + class B < A + attr_reader :res + def initialize(a, b, *) + super + @res = [a, b] + end + end + + B.new(1, 2).res + CODE + end + + def test_KeywordHashNode + assert_prism_eval("[a: [:b, :c]]") + end + + def test_SuperNode + assert_prism_eval("def to_s; super 1; end") + assert_prism_eval("def to_s; super(); end") + assert_prism_eval("def to_s; super('a', :b, [1,2,3]); end") + assert_prism_eval("def to_s; super(1, 2, 3, &:foo); end") + end + + ############################################################################ + # Methods / parameters # + ############################################################################ + + def test_AliasGlobalVariableNode + assert_prism_eval("alias $prism_foo $prism_bar") + end + + def test_AliasMethodNode + assert_prism_eval("alias :prism_a :to_s") + end + + def test_BlockParameterNode + assert_prism_eval("def prism_test_block_parameter_node(&bar) end") + assert_prism_eval("->(b, c=1, *d, e, &f){}") + + # Test BlockParameterNode with no name + assert_prism_eval("->(&){}") + assert_prism_eval("def prism_test_block_parameter_node(&); end") + end + + def test_BlockParametersNode + assert_prism_eval("Object.tap { || }") + assert_prism_eval("[1].map { |num| num }") + assert_prism_eval("[1].map { |a; b| b = 2; a + b}") + + # Test block parameters with multiple _ + assert_prism_eval(<<~RUBY) + [[1, 2, 3, 4, 5, 6]].map { |(_, _, _, _, _, _)| _ } + RUBY + end + + def test_FowardingParameterNode + assert_prism_eval("def prism_test_forwarding_parameter_node(...); end") + end + + def test_KeywordRestParameterNode + assert_prism_eval("def prism_test_keyword_rest_parameter_node(a, **b); end") + assert_prism_eval("Object.tap { |**| }") + + # Test that KeywordRestParameterNode creates a copy + assert_prism_eval(<<~RUBY) + hash = {} + o = Object.new + def o.foo(**a) = a[:foo] = 1 + + o.foo(**hash) + hash + RUBY + end + + def test_NoKeywordsParameterNode + assert_prism_eval("def prism_test_no_keywords(**nil); end") + assert_prism_eval("def prism_test_no_keywords(a, b = 2, **nil); end") + end + + def test_OptionalParameterNode + assert_prism_eval("def prism_test_optional_param_node(bar = nil); end") + end + + def test_OptionalKeywordParameterNode + assert_prism_eval("def prism_test_optional_keyword_param_node(bar: nil); end") + + # Test with optional argument and method call in OptionalKeywordParameterNode + assert_prism_eval(<<~RUBY) + o = Object.new + def o.foo = 1 + def o.bar(a = nil, b: foo) = b + o.bar + RUBY + end + + def test_ParametersNode + assert_prism_eval("def prism_test_parameters_node(bar, baz); end") + assert_prism_eval("def prism_test_parameters_node(a, b = 2); end") + end + + def test_RequiredParameterNode + assert_prism_eval("def prism_test_required_param_node(bar); end") + assert_prism_eval("def prism_test_required_param_node(foo, bar); end") + end + + def test_RequiredKeywordParameterNode + assert_prism_eval("def prism_test_required_param_node(bar:); end") + assert_prism_eval("def prism_test_required_param_node(foo:, bar:); end") + assert_prism_eval("-> a, b = 1, c:, d:, &e { a }") + end + + def test_RestParameterNode + assert_prism_eval("def prism_test_rest_parameter_node(*a); end") + end + + def test_UndefNode + assert_prism_eval("def prism_undef_node_1; end; undef prism_undef_node_1") + assert_prism_eval(<<-HERE + def prism_undef_node_2 + end + def prism_undef_node_3 + end + undef prism_undef_node_2, prism_undef_node_3 + HERE + ) + assert_prism_eval(<<-HERE + def prism_undef_node_4 + end + undef :'prism_undef_node_#{4}' + HERE + ) + end + + ############################################################################ + # Pattern matching # + ############################################################################ + + def test_AlternationPatternNode + assert_prism_eval("1 in 1 | 2") + assert_prism_eval("1 in 2 | 1") + assert_prism_eval("1 in 2 | 3 | 4 | 1") + assert_prism_eval("1 in 2 | 3") + end + + def test_ArrayPatternNode + assert_prism_eval("[] => []") + + ["in", "=>"].each do |operator| + ["", "Array"].each do |constant| + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[1, 2, 3]") + + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[1, *]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[1, 2, *]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[1, 2, 3, *]") + + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*foo]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[1, *foo]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[1, 2, *foo]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[1, 2, 3, *foo]") + + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*, 3]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*, 2, 3]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*, 1, 2, 3]") + + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*foo, 3]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*foo, 2, 3]") + assert_prism_eval("[1, 2, 3] #{operator} #{constant}[*foo, 1, 2, 3]") + end + end + + assert_prism_eval("begin; Object.new => [1, 2, 3]; rescue NoMatchingPatternError; true; end") + assert_prism_eval("begin; [1, 2, 3] => Object[1, 2, 3]; rescue NoMatchingPatternError; true; end") + end + + def test_CapturePatternNode + assert_prism_eval("[1] => [Integer => foo]") + end + + def test_CaseMatchNode + assert_prism_eval(<<~RUBY) + case [1, 2, 3] + in [1, 2, 3] + 4 + end + RUBY + + assert_prism_eval(<<~RUBY) + case { a: 5, b: 6 } + in [1, 2, 3] + 4 + in { a: 5, b: 6 } + 7 + end + RUBY + + assert_prism_eval(<<~RUBY) + case [1, 2, 3, 4] + in [1, 2, 3] + 4 + in { a: 5, b: 6 } + 7 + else + end + RUBY + + assert_prism_eval(<<~RUBY) + case [1, 2, 3, 4] + in [1, 2, 3] + 4 + in { a: 5, b: 6 } + 7 + else + 8 + end + RUBY + + assert_prism_eval(<<~RUBY) + case [1, 2, 3] + in [1, 2, 3] unless to_s + in [1, 2, 3] if to_s.nil? + in [1, 2, 3] + true + end + RUBY + end + + def test_FindPatternNode + ["in", "=>"].each do |operator| + ["", "Array"].each do |constant| + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 1, 2, 3, 4, 5, *]") + + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 1, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 3, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 5, *]") + + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 1, 2, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 2, 3, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 3, 4, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 4, 5, *]") + + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 1, 2, 3, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 2, 3, 4, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 3, 4, 5, *]") + + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 1, 2, 3, 4, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 2, 3, 4, 5, *]") + + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 3, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 3, 4, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 3, 4, 5, *]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 1, 2, 3, 4, *]") + + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 3, *foo]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 3, 4, *foo]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 3, 4, 5, *foo]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*, 1, 2, 3, 4, *foo]") + + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 3, *bar]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 3, 4, *bar]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 3, 4, 5, *bar]") + assert_prism_eval("[1, 2, 3, 4, 5] #{operator} #{constant}[*foo, 1, 2, 3, 4, *bar]") + end + end + + assert_prism_eval("[1, [2, [3, [4, [5]]]]] => [*, [*, [*, [*, [*]]]]]") + assert_prism_eval("[1, [2, [3, [4, [5]]]]] => [1, [2, [3, [4, [5]]]]]") + + assert_prism_eval("begin; Object.new => [*, 2, *]; rescue NoMatchingPatternError; true; end") + assert_prism_eval("begin; [1, 2, 3] => Object[*, 2, *]; rescue NoMatchingPatternError; true; end") + end + + def test_HashPatternNode + assert_prism_eval("{} => {}") + + [["{ ", " }"], ["Hash[", "]"]].each do |(prefix, suffix)| + assert_prism_eval("{} => #{prefix} **nil #{suffix}") + + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1 #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, b: 2 #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} b: 2, c: 3 #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, b: 2, c: 3 #{suffix}") + + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} ** #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, ** #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, b: 2, ** #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} b: 2, c: 3, ** #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, b: 2, c: 3, ** #{suffix}") + + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} **foo #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, **foo #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, b: 2, **foo #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} b: 2, c: 3, **foo #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, b: 2, c: 3, **foo #{suffix}") + + assert_prism_eval("{ a: 1 } => #{prefix} a: 1, **nil #{suffix}") + assert_prism_eval("{ a: 1, b: 2, c: 3 } => #{prefix} a: 1, b: 2, c: 3, **nil #{suffix}") + end + + assert_prism_eval("{ a: { b: { c: 1 } } } => { a: { b: { c: 1 } } }") + end + + def test_MatchPredicateNode + assert_prism_eval("1 in 1") + assert_prism_eval("1.0 in 1.0") + assert_prism_eval("1i in 1i") + assert_prism_eval("1r in 1r") + + assert_prism_eval("\"foo\" in \"foo\"") + assert_prism_eval("\"foo \#{1}\" in \"foo \#{1}\"") + + assert_prism_eval("false in false") + assert_prism_eval("nil in nil") + assert_prism_eval("self in self") + assert_prism_eval("true in true") + + assert_prism_eval("5 in 0..10") + assert_prism_eval("5 in 0...10") + + assert_prism_eval("[\"5\"] in %w[5]") + + assert_prism_eval("Prism in Prism") + assert_prism_eval("Prism in ::Prism") + + assert_prism_eval(":prism in :prism") + assert_prism_eval("%s[prism\#{1}] in %s[prism\#{1}]") + assert_prism_eval("\"foo\" in /.../") + assert_prism_eval("\"foo1\" in /...\#{1}/") + assert_prism_eval("4 in ->(v) { v.even? }") + + assert_prism_eval("5 in foo") + + assert_prism_eval("1 in 2") + + # Bug: https://bugs.ruby-lang.org/issues/20956 + assert_prism_eval("1 in [1 | [1]]") + end + + def test_MatchRequiredNode + assert_prism_eval("1 => 1") + assert_prism_eval("1.0 => 1.0") + assert_prism_eval("1i => 1i") + assert_prism_eval("1r => 1r") + + assert_prism_eval("\"foo\" => \"foo\"") + assert_prism_eval("\"foo \#{1}\" => \"foo \#{1}\"") + + assert_prism_eval("false => false") + assert_prism_eval("nil => nil") + assert_prism_eval("true => true") + + assert_prism_eval("5 => 0..10") + assert_prism_eval("5 => 0...10") + + assert_prism_eval("[\"5\"] => %w[5]") + + assert_prism_eval(":prism => :prism") + assert_prism_eval("%s[prism\#{1}] => %s[prism\#{1}]") + assert_prism_eval("\"foo\" => /.../") + assert_prism_eval("\"foo1\" => /...\#{1}/") + assert_prism_eval("4 => ->(v) { v.even? }") + + assert_prism_eval("5 => foo") + end + + def test_PinnedExpressionNode + assert_prism_eval("4 in ^(4)") + end + + def test_PinnedVariableNode + assert_prism_eval("module Prism; @@prism = 1; 1 in ^@@prism; end") + assert_prism_eval("module Prism; @prism = 1; 1 in ^@prism; end") + assert_prism_eval("$prism = 1; 1 in ^$prism") + assert_prism_eval("prism = 1; 1 in ^prism") + assert_prism_eval("[1].each { 1 => ^it }") + end + + ############################################################################ + # Miscellaneous # + ############################################################################ + + def test_eval + assert_prism_eval("eval('1 + 1')", raw: true) + assert_prism_eval("a = 1; eval('a + 1')", raw: true) + + assert_prism_eval(<<~CODE, raw: true) + def prism_eval_splat(**bar) + eval("bar") + end + prism_eval_splat(bar: 10) + CODE + + assert_prism_eval(<<~CODE, raw: true) + def prism_eval_keywords(baz:) + eval("baz") + end + prism_eval_keywords(baz: 10) + CODE + + assert_prism_eval(<<~CODE, raw: true) + [1].each do |a| + [2].each do |b| + c = 3 + eval("a + b + c") + end + end + CODE + + assert_prism_eval(<<~CODE, raw: true) + def prism_eval_binding(b) + eval("bar", b) + end + + bar = :ok + prism_eval_binding(binding) + CODE + end + + def test_ScopeNode + assert_separately(%w[], <<~'RUBY') + def compare_eval(source) + ruby_eval = RubyVM::InstructionSequence.compile("module A; " + source + "; end").eval + prism_eval = RubyVM::InstructionSequence.compile_prism("module B; " + source + "; end").eval + + assert_equal ruby_eval, prism_eval + end + + def assert_prism_eval(source) + $VERBOSE, verbose_bak = nil, $VERBOSE + + begin + compare_eval(source) + + # Test "popped" functionality + compare_eval("#{source}; 1") + ensure + $VERBOSE = verbose_bak + end + end + + assert_prism_eval("a = 1; 1.times do; { a: }; end") + assert_prism_eval("a = 1; def foo(a); a; end") + RUBY + end + + ############################################################################ + # Errors # + ############################################################################ + + def test_MissingNode + # TODO + end + + ############################################################################ + # Encoding # + ############################################################################ + + def test_encoding + assert_prism_eval('"però"') + assert_prism_eval(":però") + end + + def test_parse_file + assert_nothing_raised do + RubyVM::InstructionSequence.compile_file_prism(__FILE__) + end + + error = assert_raise Errno::ENOENT do + RubyVM::InstructionSequence.compile_file_prism("idontexist.rb") + end + + assert_equal "No such file or directory - idontexist.rb", error.message + + assert_raise TypeError do + RubyVM::InstructionSequence.compile_file_prism(nil) + end + end + + private + + def compare_eval(source, raw:, location:) + source = raw ? source : "class Prism::TestCompilePrism\n#{source}\nend" + + ruby_eval = RubyVM::InstructionSequence.compile_parsey(source).eval + prism_eval = RubyVM::InstructionSequence.compile_prism(source).eval + + if ruby_eval.is_a? Proc + assert_equal ruby_eval.class, prism_eval.class, "@#{location.path}:#{location.lineno}" + else + assert_equal ruby_eval, prism_eval, "@#{location.path}:#{location.lineno}" + end + end + + def assert_prism_eval(source, raw: false) + location = caller_locations(1, 1).first + $VERBOSE, verbose_bak = nil, $VERBOSE + + begin + compare_eval(source, raw:, location:) + + # Test "popped" functionality + compare_eval("#{source}; 1", raw:, location:) + ensure + $VERBOSE = verbose_bak + end + end + end +end diff --git a/test/ruby/test_complex.rb b/test/ruby/test_complex.rb index edbdffd069..bb131cee91 100644 --- a/test/ruby/test_complex.rb +++ b/test/ruby/test_complex.rb @@ -526,6 +526,71 @@ class Complex_Test < Test::Unit::TestCase r = c ** Rational(-2,3) assert_in_delta(0.432, r.real, 0.001) assert_in_delta(-0.393, r.imag, 0.001) + end + + def test_expt_for_special_angle + c = Complex(1, 0) ** 100000000000000000000000000000000 + assert_equal(Complex(1, 0), c) + + c = Complex(-1, 0) ** 10000000000000000000000000000000 + assert_equal(Complex(1, 0), c) + + c = Complex(-1, 0) ** 10000000000000000000000000000001 + assert_equal(Complex(-1, 0), c) + + c = Complex(0, 1) ** 100000000000000000000000000000000 + assert_equal(Complex(1, 0), c) + + c = Complex(0, 1) ** 100000000000000000000000000000001 + assert_equal(Complex(0, 1), c) + + c = Complex(0, 1) ** 100000000000000000000000000000002 + assert_equal(Complex(-1, 0), c) + + c = Complex(0, 1) ** 100000000000000000000000000000003 + assert_equal(Complex(0, -1), c) + + c = Complex(0, -1) ** 100000000000000000000000000000000 + assert_equal(Complex(1, 0), c) + + c = Complex(0, -1) ** 100000000000000000000000000000001 + assert_equal(Complex(0, -1), c) + + c = Complex(0, -1) ** 100000000000000000000000000000002 + assert_equal(Complex(-1, 0), c) + + c = Complex(0, -1) ** 100000000000000000000000000000003 + assert_equal(Complex(0, 1), c) + + c = Complex(1, 1) ** 1 + assert_equal(Complex(1, 1), c) + + c = Complex(1, 1) ** 2 + assert_equal(Complex(0, 2), c) + + c = Complex(1, 1) ** 3 + assert_equal(Complex(-2, 2), c) + + c = Complex(1, 1) ** 4 + assert_equal(Complex(-4, 0), c) + + c = Complex(1, 1) ** 5 + assert_equal(Complex(-4, -4), c) + + c = Complex(1, 1) ** 6 + assert_equal(Complex(0, -8), c) + + c = Complex(1, 1) ** 7 + assert_equal(Complex(8, -8), c) + + c = Complex(-2, -2) ** 3 + assert_equal(Complex(16, -16), c) + + c = Complex(2, -2) ** 3 + assert_equal(Complex(-16, -16), c) + + c = Complex(-2, 2) ** 3 + assert_equal(Complex(16, 16), c) c = Complex(0.0, -888888888888888.0)**8888 assert_not_predicate(c.real, :nan?) @@ -676,6 +741,17 @@ class Complex_Test < Test::Unit::TestCase assert_equal('(1+2i)', c.inspect) end + def test_inspect_to_s_frozen_bug_20337 + assert_separately([], <<~'RUBY') + class Numeric + def inspect = super.freeze + end + c = Complex(Numeric.new, 1) + assert_match(/\A\(#<Numeric:/, c.inspect) + assert_match(/\A#<Numeric:/, c.to_s) + RUBY + end + def test_marshal c = Complex(1,2) @@ -915,31 +991,27 @@ class Complex_Test < Test::Unit::TestCase } end - def test_Complex_without_exception - assert_nothing_raised(ArgumentError){ - assert_equal(nil, Complex('5x', exception: false)) - } - assert_nothing_raised(ArgumentError){ - assert_equal(nil, Complex(nil, exception: false)) - } - assert_nothing_raised(ArgumentError){ - assert_equal(nil, Complex(Object.new, exception: false)) - } - assert_nothing_raised(ArgumentError){ - assert_equal(nil, Complex(1, nil, exception: false)) - } - assert_nothing_raised(ArgumentError){ - assert_equal(nil, Complex(1, Object.new, exception: false)) - } + def assert_complex_with_exception(error, *args, message: "") + assert_raise(error, message) do + Complex(*args, exception: true) + end + assert_nothing_raised(error, message) do + assert_nil(Complex(*args, exception: false)) + assert_nil($!) + end + end + + def test_Complex_with_exception + assert_complex_with_exception(ArgumentError, '5x') + assert_complex_with_exception(TypeError, nil) + assert_complex_with_exception(TypeError, Object.new) + assert_complex_with_exception(TypeError, 1, nil) + assert_complex_with_exception(TypeError, 1, Object.new) o = Object.new def o.to_c; raise; end - assert_nothing_raised(ArgumentError){ - assert_equal(nil, Complex(o, exception: false)) - } - assert_nothing_raised(ArgumentError){ - assert_equal(nil, Complex(1, o, exception: false)) - } + assert_complex_with_exception(RuntimeError, o) + assert_complex_with_exception(TypeError, 1, o) end def test_respond @@ -993,6 +1065,29 @@ class Complex_Test < Test::Unit::TestCase assert_raise(RangeError){Rational(Complex(3,2))} end + def test_to_r_with_float + assert_equal(Rational(3), Complex(3, 0.0).to_r) + assert_raise(RangeError){Complex(3, 1.0).to_r} + end + + def test_to_r_with_numeric_obj + c = Class.new(Numeric) + + num = 0 + c.define_method(:to_s) { num.to_s } + c.define_method(:==) { num == it } + c.define_method(:<) { num < it } + + o = c.new + assert_equal(Rational(3), Complex(3, o).to_r) + + num = 1 + assert_raise(RangeError){Complex(3, o).to_r} + + c.define_method(:to_r) { 0r } + assert_equal(Rational(3), Complex(3, o).to_r) + end + def test_to_c c = nil.to_c assert_equal([0,0], [c.real, c.imag]) diff --git a/test/ruby/test_continuation.rb b/test/ruby/test_continuation.rb index 8c62d20840..612dbf28c9 100644 --- a/test/ruby/test_continuation.rb +++ b/test/ruby/test_continuation.rb @@ -4,6 +4,10 @@ EnvUtil.suppress_warning {require 'continuation'} require 'fiber' class TestContinuation < Test::Unit::TestCase + def setup + omit 'requires callcc support' unless respond_to?(:callcc) + end + def test_create assert_equal(:ok, callcc{:ok}) assert_equal(:ok, callcc{|c| c.call :ok}) diff --git a/test/ruby/test_data.rb b/test/ruby/test_data.rb index 9380076e1b..fbc3205d63 100644 --- a/test/ruby/test_data.rb +++ b/test/ruby/test_data.rb @@ -97,6 +97,7 @@ class TestData < Test::Unit::TestCase # Missing arguments can be fixed in initialize assert_equal([[], {foo: 1}], klass.new(foo: 1).passed) + assert_equal([[], {foo: 42}], klass.new(42).passed) # Extra keyword arguments can be dropped in initialize assert_equal([[], {foo: 1, bar: 2, baz: 3}], klass.new(foo: 1, bar: 2, baz: 3).passed) @@ -116,14 +117,31 @@ class TestData < Test::Unit::TestCase assert_equal({foo: 1, bar: 2}, test.to_h) assert_equal({"foo"=>"1", "bar"=>"2"}, test.to_h { [_1.to_s, _2.to_s] }) + assert_equal([1, 2], test.deconstruct) assert_equal({foo: 1, bar: 2}, test.deconstruct_keys(nil)) assert_equal({foo: 1}, test.deconstruct_keys(%i[foo])) assert_equal({foo: 1}, test.deconstruct_keys(%i[foo baz])) + assert_equal({}, test.deconstruct_keys(%i[foo bar baz])) assert_raise(TypeError) { test.deconstruct_keys(0) } assert_kind_of(Integer, test.hash) end + def test_hash + measure = Data.define(:amount, :unit) + + assert_equal(measure[1, 'km'].hash, measure[1, 'km'].hash) + assert_not_equal(measure[1, 'km'].hash, measure[10, 'km'].hash) + assert_not_equal(measure[1, 'km'].hash, measure[1, 'm'].hash) + assert_not_equal(measure[1, 'km'].hash, measure[1.0, 'km'].hash) + + # Structurally similar data class, but shouldn't be considered + # the same hash key + measurement = Data.define(:amount, :unit) + + assert_not_equal(measure[1, 'km'].hash, measurement[1, 'km'].hash) + end + def test_inspect klass = Data.define(:a) o = klass.new(1) @@ -241,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 @@ -262,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_default_gems.rb b/test/ruby/test_default_gems.rb index c1c81bfec0..b82e304cbd 100644 --- a/test/ruby/test_default_gems.rb +++ b/test/ruby/test_default_gems.rb @@ -2,22 +2,28 @@ require 'rubygems' class TestDefaultGems < Test::Unit::TestCase + def self.load(file) + code = File.read(file, mode: "r:UTF-8:-", &:read) + + # These regex patterns are from load_gemspec method of rbinstall.rb. + # - `git ls-files` is useless under ruby's repository + # - `2>/dev/null` works only on Unix-like platforms + code.gsub!(/(?:`git[^\`]*`|%x\[git[^\]]*\])\.split\([^\)]*\)/m, '[]') + code.gsub!(/IO\.popen\(.*git.*?\)/, '[] || itself') + + eval(code, binding, file) + end def test_validate_gemspec srcdir = File.expand_path('../../..', __FILE__) specs = 0 Dir.chdir(srcdir) do - unless system("git", "rev-parse", %i[out err]=>IO::NULL) - omit "git not found" - end - Dir.glob("#{srcdir}/{lib,ext}/**/*.gemspec").map do |src| + all_assertions_foreach(nil, *Dir["{lib,ext}/**/*.gemspec"]) do |src| specs += 1 - assert_nothing_raised do - raise("invalid spec in #{src}") unless Gem::Specification.load(src) - end + assert_kind_of(Gem::Specification, self.class.load(src), "invalid spec in #{src}") end end - assert specs > 0, "gemspecs not found" + assert_operator specs, :>, 0, "gemspecs not found" end end diff --git a/test/ruby/test_defined.rb b/test/ruby/test_defined.rb index 3324a09afe..db1fdc8e25 100644 --- a/test/ruby/test_defined.rb +++ b/test/ruby/test_defined.rb @@ -127,6 +127,53 @@ class TestDefined < Test::Unit::TestCase assert_equal nil, defined?($2) end + def test_defined_assignment + assert_equal("assignment", defined?(a = 1)) + assert_equal("assignment", defined?(a += 1)) + assert_equal("assignment", defined?(a &&= 1)) + assert_equal("assignment", eval('defined?(A = 1)')) + assert_equal("assignment", eval('defined?(A += 1)')) + assert_equal("assignment", eval('defined?(A &&= 1)')) + assert_equal("assignment", eval('defined?(A::B = 1)')) + assert_equal("assignment", eval('defined?(A::B += 1)')) + assert_equal("assignment", eval('defined?(A::B &&= 1)')) + end + + def test_defined_splat + assert_nil(defined?([*a])) + assert_nil(defined?(itself(*a))) + assert_equal("expression", defined?([*itself])) + assert_equal("method", defined?(itself(*itself))) + end + + def test_defined_hash + assert_nil(defined?({a: a})) + assert_nil(defined?({a => 1})) + assert_nil(defined?({a => a})) + assert_nil(defined?({**a})) + assert_nil(defined?(itself(a: a))) + assert_nil(defined?(itself(a => 1))) + assert_nil(defined?(itself(a => a))) + assert_nil(defined?(itself(**a))) + assert_nil(defined?(itself({a: a}))) + assert_nil(defined?(itself({a => 1}))) + assert_nil(defined?(itself({a => a}))) + assert_nil(defined?(itself({**a}))) + + assert_equal("expression", defined?({a: itself})) + assert_equal("expression", defined?({itself => 1})) + assert_equal("expression", defined?({itself => itself})) + assert_equal("expression", defined?({**itself})) + assert_equal("method", defined?(itself(a: itself))) + assert_equal("method", defined?(itself(itself => 1))) + assert_equal("method", defined?(itself(itself => itself))) + assert_equal("method", defined?(itself(**itself))) + assert_equal("method", defined?(itself({a: itself}))) + assert_equal("method", defined?(itself({itself => 1}))) + assert_equal("method", defined?(itself({itself => itself}))) + assert_equal("method", defined?(itself({**itself}))) + end + def test_defined_literal assert_equal("nil", defined?(nil)) assert_equal("true", defined?(true)) @@ -196,6 +243,26 @@ class TestDefined < Test::Unit::TestCase assert_nil(defined?(p () + 1)) end + def test_defined_paren_void_stmts + assert_equal("expression", defined? (;x)) + assert_equal("expression", defined? (x;)) + assert_nil(defined? ( + + x + + )) + + x = 1 + + assert_equal("expression", defined? (;x)) + assert_equal("expression", defined? (x;)) + assert_equal("local-variable", defined? ( + + x + + )) + end + def test_defined_impl_specific feature7035 = '[ruby-core:47558]' # not spec assert_predicate(defined?(Foo), :frozen?, feature7035) @@ -303,6 +370,20 @@ class TestDefined < Test::Unit::TestCase assert_equal("super", o.x, bug8367) end + def test_super_in_basic_object + BasicObject.class_eval do + def a + defined?(super) + end + end + + assert_nil(a) + ensure + BasicObject.class_eval do + undef_method :a if defined?(a) + end + end + def test_super_toplevel assert_separately([], "assert_nil(defined?(super))") end diff --git a/test/ruby/test_dir.rb b/test/ruby/test_dir.rb index 63ef99f159..edb5210af1 100644 --- a/test/ruby/test_dir.rb +++ b/test/ruby/test_dir.rb @@ -96,7 +96,7 @@ class TestDir < Test::Unit::TestCase d.close end - def test_chdir + def test_class_chdir pwd = Dir.pwd setup_envs @@ -104,22 +104,23 @@ class TestDir < Test::Unit::TestCase assert_raise(ArgumentError) { Dir.chdir } ENV["HOME"] = pwd Dir.chdir do - assert_warning(/conflicting chdir during another chdir block/) { Dir.chdir(pwd) } + conflicting = /conflicting chdir during another chdir block\n^#{Regexp.quote(__FILE__)}:#{__LINE__-1}:/ + assert_warning(conflicting) { Dir.chdir(pwd) } - assert_warning(/conflicting chdir during another chdir block/) { Dir.chdir(@root) } + assert_warning(conflicting) { Dir.chdir(@root) } assert_equal(@root, Dir.pwd) - assert_warning(/conflicting chdir during another chdir block/) { Dir.chdir(pwd) } + assert_warning(conflicting) { Dir.chdir(pwd) } assert_raise(RuntimeError) { Thread.new { Thread.current.report_on_exception = false; Dir.chdir(@root) }.join } assert_raise(RuntimeError) { Thread.new { Thread.current.report_on_exception = false; Dir.chdir(@root) { } }.join } - assert_warning(/conflicting chdir during another chdir block/) { Dir.chdir(pwd) } + assert_warning(conflicting) { Dir.chdir(pwd) } - assert_warning(/conflicting chdir during another chdir block/) { Dir.chdir(@root) } + assert_warning(conflicting) { Dir.chdir(@root) } assert_equal(@root, Dir.pwd) - assert_warning(/conflicting chdir during another chdir block/) { Dir.chdir(pwd) } + assert_warning(conflicting) { Dir.chdir(pwd) } Dir.chdir(@root) do assert_equal(@root, Dir.pwd) end @@ -134,6 +135,73 @@ class TestDir < Test::Unit::TestCase end end + def test_instance_chdir + pwd = Dir.pwd + dir = Dir.new(pwd) + root_dir = Dir.new(@root) + setup_envs + + ENV["HOME"] = pwd + ret = root_dir.chdir do |*a| + conflicting = /conflicting chdir during another chdir block\n^#{Regexp.quote(__FILE__)}:#{__LINE__-1}:/ + + assert_empty(a) + + assert_warning(conflicting) { dir.chdir } + assert_warning(conflicting) { root_dir.chdir } + + assert_equal(@root, Dir.pwd) + + assert_raise(RuntimeError) { Thread.new { Thread.current.report_on_exception = false; dir.chdir }.join } + assert_raise(RuntimeError) { Thread.new { Thread.current.report_on_exception = false; dir.chdir{} }.join } + + assert_warning(conflicting) { dir.chdir } + assert_equal(pwd, Dir.pwd) + + assert_warning(conflicting) { root_dir.chdir } + assert_equal(@root, Dir.pwd) + + assert_warning(conflicting) { dir.chdir } + + root_dir.chdir do + assert_equal(@root, Dir.pwd) + end + assert_equal(pwd, Dir.pwd) + + 42 + end + + assert_separately(["-", @root], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + root = ARGV.shift + + $dir_warnings = [] + + def Warning.warn(message) + $dir_warnings << message + end + + line2 = line1 = __LINE__; Dir.chdir(root) do + line2 = __LINE__; Dir.chdir + end + + message = $dir_warnings.shift + assert_include(message, "#{__FILE__}:#{line2}:") + assert_include(message, "#{__FILE__}:#{line1}:") + assert_empty($dir_warnings) + end; + + assert_equal(42, ret) + ensure + begin + assert_equal(0, dir.chdir) + rescue + abort("cannot return the original directory: #{ pwd }") + end + dir.close + root_dir.close + end + def test_chdir_conflict pwd = Dir.pwd q = Thread::Queue.new @@ -188,7 +256,7 @@ class TestDir < Test::Unit::TestCase Dir.glob(@root, sort: nil) end - assert_equal(("a".."z").step(2).map {|f| File.join(File.join(@root, f), "") }, + assert_equal(("a".."z").each_slice(2).map {|f,_| File.join(File.join(@root, f), "") }, Dir.glob(File.join(@root, "*/"))) assert_equal([File.join(@root, '//a')], Dir.glob(@root + '//a')) @@ -539,6 +607,55 @@ class TestDir < Test::Unit::TestCase ENV.delete('USERPROFILE') assert_equal("C:/ruby/homepath", Dir.home) end + + def test_home_at_startup_windows + env = {'HOME' => "C:\\ruby\\home"} + args = [env] + assert_separately(args, "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_equal("C:/ruby/home", Dir.home) + end; + + env['USERPROFILE'] = "C:\\ruby\\userprofile" + assert_separately(args, "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_equal("C:/ruby/home", Dir.home) + end; + + env['HOME'] = nil + assert_separately(args, "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_equal("C:/ruby/userprofile", Dir.home) + end; + + env['HOMEDRIVE'] = "C:" + env['HOMEPATH'] = "\\ruby\\homepath" + assert_separately(args, "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_equal("C:/ruby/userprofile", Dir.home) + end; + + env['USERPROFILE'] = nil + assert_separately(args, "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_equal("C:/ruby/homepath", Dir.home) + end; + end + + def test_children_long_name + Dir.mktmpdir do |dirname| + longest_possible_component = "b" * 255 + long_path = File.join(dirname, longest_possible_component) + Dir.mkdir(long_path) + File.write("#{long_path}/c", "") + assert_equal(%w[c], Dir.children(long_path)) + ensure + File.unlink("#{long_path}/c") + Dir.rmdir(long_path) + end + rescue Errno::ENOENT + omit "File system does not support long file name" + end end def test_home @@ -600,6 +717,23 @@ class TestDir < Test::Unit::TestCase } end + def test_for_fd + if Dir.respond_to? :for_fd + begin + new_dir = Dir.new('..') + for_fd_dir = Dir.for_fd(new_dir.fileno) + assert_equal(new_dir.chdir{Dir.pwd}, for_fd_dir.chdir{Dir.pwd}) + ensure + new_dir&.close + if for_fd_dir + assert_raise(Errno::EBADF) { for_fd_dir.close } + end + end + else + assert_raise(NotImplementedError) { Dir.for_fd(0) } + end + end + def test_empty? assert_not_send([Dir, :empty?, @root]) a = File.join(@root, "a") diff --git a/test/ruby/test_dir_m17n.rb b/test/ruby/test_dir_m17n.rb index 67bad8a514..cdf8b44ef2 100644 --- a/test/ruby/test_dir_m17n.rb +++ b/test/ruby/test_dir_m17n.rb @@ -56,7 +56,7 @@ class TestDir_M17N < Test::Unit::TestCase return if Bug::File::Fs.fsname(Dir.tmpdir) == "apfs" with_tmpdir {|d| assert_separately(%w[-EASCII-8BIT], <<-'EOS', :chdir=>d) - filename = "\xff".force_encoding("ASCII-8BIT") # invalid byte sequence as UTF-8 + filename = "\xff".dup.force_encoding("ASCII-8BIT") # invalid byte sequence as UTF-8 File.open(filename, "w") {} opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) @@ -64,7 +64,7 @@ class TestDir_M17N < Test::Unit::TestCase assert_include(ents, filename) EOS assert_separately(%w[-EUTF-8], <<-'EOS', :chdir=>d) - filename = "\xff".force_encoding("UTF-8") # invalid byte sequence as UTF-8 + filename = "\xff".dup.force_encoding("UTF-8") # invalid byte sequence as UTF-8 File.open(filename, "w") {} opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) @@ -77,7 +77,7 @@ class TestDir_M17N < Test::Unit::TestCase def test_filename_as_bytes_extutf8 with_tmpdir {|d| assert_separately(%w[-EUTF-8], <<-'EOS', :chdir=>d) - filename = "\xc2\xa1".force_encoding("utf-8") + filename = "\xc2\xa1".dup.force_encoding("utf-8") File.open(filename, "w") {} opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) @@ -85,9 +85,9 @@ class TestDir_M17N < Test::Unit::TestCase EOS assert_separately(%w[-EUTF-8], <<-'EOS', :chdir=>d) if /mswin|mingw|darwin/ =~ RUBY_PLATFORM - filename = "\x8f\xa2\xc2".force_encoding("euc-jp") + filename = "\x8f\xa2\xc2".dup.force_encoding("euc-jp") else - filename = "\xc2\xa1".force_encoding("euc-jp") + filename = "\xc2\xa1".dup.force_encoding("euc-jp") end assert_nothing_raised(Errno::ENOENT) do open(filename) {} @@ -96,8 +96,8 @@ class TestDir_M17N < Test::Unit::TestCase # no meaning test on windows unless /mswin|mingw|darwin/ =~ RUBY_PLATFORM assert_separately(%W[-EUTF-8], <<-'EOS', :chdir=>d) - filename1 = "\xc2\xa1".force_encoding("utf-8") - filename2 = "\xc2\xa1".force_encoding("euc-jp") + filename1 = "\xc2\xa1".dup.force_encoding("utf-8") + filename2 = "\xc2\xa1".dup.force_encoding("euc-jp") filename3 = filename1.encode("euc-jp") filename4 = filename2.encode("utf-8") assert_file.stat(filename1) @@ -121,13 +121,13 @@ class TestDir_M17N < Test::Unit::TestCase assert_include(ents, filename) EOS assert_separately(%w[-EUTF-8:EUC-JP], <<-'EOS', :chdir=>d) - filename = "\xA4\xA2".force_encoding("euc-jp") + filename = "\xA4\xA2".dup.force_encoding("euc-jp") opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) assert_include(ents, filename) EOS assert_separately(%w[-EUTF-8:EUC-JP], <<-'EOS', :chdir=>d) - filename = "\xA4\xA2".force_encoding("euc-jp") + filename = "\xA4\xA2".dup.force_encoding("euc-jp") assert_nothing_raised(Errno::ENOENT) do open(filename) {} end @@ -149,7 +149,7 @@ class TestDir_M17N < Test::Unit::TestCase EOS assert_separately(%w[-EUTF-8:EUC-JP], <<-'EOS', :chdir=>d) filename1 = "\u2661" # WHITE HEART SUIT which is not representable in EUC-JP - filename2 = "\xA4\xA2".force_encoding("euc-jp") # HIRAGANA LETTER A in EUC-JP + filename2 = "\xA4\xA2".dup.force_encoding("euc-jp") # HIRAGANA LETTER A in EUC-JP opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) assert_include(ents, filename1) @@ -158,7 +158,7 @@ class TestDir_M17N < Test::Unit::TestCase assert_separately(%w[-EUTF-8:EUC-JP], <<-'EOS', :chdir=>d) filename1 = "\u2661" # WHITE HEART SUIT which is not representable in EUC-JP filename2 = "\u3042" # HIRAGANA LETTER A which is representable in EUC-JP - filename3 = "\xA4\xA2".force_encoding("euc-jp") # HIRAGANA LETTER A in EUC-JP + filename3 = "\xA4\xA2".dup.force_encoding("euc-jp") # HIRAGANA LETTER A in EUC-JP assert_file.stat(filename1) assert_file.stat(filename2) assert_file.stat(filename3) @@ -172,7 +172,7 @@ class TestDir_M17N < Test::Unit::TestCase return if /cygwin/ =~ RUBY_PLATFORM with_tmpdir {|d| assert_separately(%w[-EEUC-JP], <<-'EOS', :chdir=>d) - filename = "\xA4\xA2".force_encoding("euc-jp") + filename = "\xA4\xA2".dup.force_encoding("euc-jp") File.open(filename, "w") {} opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) @@ -189,7 +189,7 @@ class TestDir_M17N < Test::Unit::TestCase return if /cygwin/ =~ RUBY_PLATFORM with_tmpdir {|d| assert_separately(%w[-EEUC-JP], <<-'EOS', :chdir=>d) - filename = "\xA4\xA2".force_encoding("euc-jp") + filename = "\xA4\xA2".dup.force_encoding("euc-jp") File.open(filename, "w") {} ents = Dir.entries(".") if /darwin/ =~ RUBY_PLATFORM @@ -200,7 +200,7 @@ class TestDir_M17N < Test::Unit::TestCase assert_include(ents, filename) EOS assert_separately(%w[-EASCII-8BIT], <<-'EOS', :chdir=>d) - filename = "\xA4\xA2".force_encoding('ASCII-8BIT') + filename = "\xA4\xA2".dup.force_encoding('ASCII-8BIT') ents = Dir.entries(".") unless ents.include?(filename) case RUBY_PLATFORM @@ -231,7 +231,7 @@ class TestDir_M17N < Test::Unit::TestCase return if /cygwin/ =~ RUBY_PLATFORM with_tmpdir {|d| assert_separately(%w[-EEUC-JP], <<-'EOS', :chdir=>d) - filename = "\xA4\xA2".force_encoding("euc-jp") + filename = "\xA4\xA2".dup.force_encoding("euc-jp") File.open(filename, "w") {} opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) @@ -241,7 +241,7 @@ class TestDir_M17N < Test::Unit::TestCase assert_include(ents, filename) EOS assert_separately(%w[-EEUC-JP:UTF-8], <<-'EOS', :chdir=>d) - filename = "\u3042" + filename = "\u3042".dup opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM ents = Dir.entries(".", **(opts||{})) if /darwin/ =~ RUBY_PLATFORM @@ -318,7 +318,7 @@ class TestDir_M17N < Test::Unit::TestCase def test_glob_warning_opendir with_enc_path do |dir| - open("#{dir}/x", "w") {} + File.binwrite("#{dir}/x", "") File.chmod(0300, dir) next if File.readable?(dir) assert_warning(/#{dir}/) do @@ -329,7 +329,7 @@ class TestDir_M17N < Test::Unit::TestCase def test_glob_warning_match_all with_enc_path do |dir| - open("#{dir}/x", "w") {} + File.binwrite("#{dir}/x", "") File.chmod(0000, dir) next if File.readable?(dir) assert_warning(/#{dir}/) do @@ -350,7 +350,7 @@ class TestDir_M17N < Test::Unit::TestCase end def test_glob_escape_multibyte - name = "\x81\\".force_encoding(Encoding::Shift_JIS) + name = "\x81\\".dup.force_encoding(Encoding::Shift_JIS) with_tmpdir do open(name, "w") {} rescue next match, = Dir.glob("#{name}*") @@ -362,9 +362,9 @@ class TestDir_M17N < Test::Unit::TestCase def test_glob_encoding with_tmpdir do list = %W"file_one.ext file_two.ext \u{6587 4ef6}1.txt \u{6587 4ef6}2.txt" - list.each {|f| open(f, "w") {}} - a = "file_one*".force_encoding Encoding::IBM437 - b = "file_two*".force_encoding Encoding::EUC_JP + list.each {|f| File.binwrite(f, "")} + a = "file_one*".dup.force_encoding Encoding::IBM437 + b = "file_two*".dup.force_encoding Encoding::EUC_JP assert_equal([a, b].map(&:encoding), Dir[a, b].map(&:encoding)) if Bug::File::Fs.fsname(Dir.pwd) == "apfs" # High Sierra's APFS cannot use filenames with undefined character @@ -375,7 +375,7 @@ class TestDir_M17N < Test::Unit::TestCase Dir.mkdir(dir) list << dir bug12081 = '[ruby-core:73868] [Bug #12081]' - a = "*".force_encoding("us-ascii") + a = "*".dup.force_encoding("us-ascii") result = Dir[a].map {|n| if n.encoding == Encoding::ASCII_8BIT || n.encoding == Encoding::ISO_8859_1 || diff --git a/test/ruby/test_econv.rb b/test/ruby/test_econv.rb index 1aad0de347..1d0641e918 100644 --- a/test/ruby/test_econv.rb +++ b/test/ruby/test_econv.rb @@ -931,7 +931,7 @@ class TestEncodingConverter < Test::Unit::TestCase def test_default_external Encoding.list.grep(->(enc) {/\AISO-8859-\d+\z/i =~ enc.name}) do |enc| - assert_separately(%W[--disable=gems -d - #{enc.name}], <<-EOS, ignore_stderr: true) + assert_separately(%W[-d - #{enc.name}], <<-EOS, ignore_stderr: true) Encoding.default_external = ext = ARGV[0] Encoding.default_internal = int ='utf-8' assert_nothing_raised do diff --git a/test/ruby/test_encoding.rb b/test/ruby/test_encoding.rb index e1a5ac4a5f..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 @@ -106,7 +106,7 @@ class TestEncoding < Test::Unit::TestCase end def test_errinfo_after_autoload - assert_separately(%w[--disable=gems], "#{<<~"begin;"}\n#{<<~'end;'}") + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") bug9038 = '[ruby-core:57949] [Bug #9038]' begin; e = assert_raise_with_message(SyntaxError, /unknown regexp option - Q/, bug9038) { @@ -126,4 +126,54 @@ class TestEncoding < Test::Unit::TestCase end end; end + + def test_ractor_load_encoding + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + 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_enum.rb b/test/ruby/test_enum.rb index f7c8f012d8..237bdc8a4d 100644 --- a/test/ruby/test_enum.rb +++ b/test/ruby/test_enum.rb @@ -843,6 +843,8 @@ class TestEnumerable < Test::Unit::TestCase end def test_callcc + omit 'requires callcc support' unless respond_to?(:callcc) + assert_raise(RuntimeError) do c = nil @obj.sort_by {|x| callcc {|c2| c ||= c2 }; x } @@ -1346,4 +1348,12 @@ class TestEnumerable < Test::Unit::TestCase klass.new.grep(/(b.)/) { svars << $1 } assert_equal(["ba", "ba"], svars) end + + def test_all_fast + data = { "key" => { "key2" => 1 } } + kk = vv = nil + data.all? { |(k, v)| kk, vv = k, v } + assert_equal(kk, "key") + assert_equal(vv, { "key2" => 1 }) + end end diff --git a/test/ruby/test_enumerator.rb b/test/ruby/test_enumerator.rb index 010c1e4969..9b972d7b22 100644 --- a/test/ruby/test_enumerator.rb +++ b/test/ruby/test_enumerator.rb @@ -127,6 +127,17 @@ class TestEnumerator < Test::Unit::TestCase assert_equal([[1,5],[2,6],[3,7]], @obj.to_enum(:foo, 1, 2, 3).with_index(5).to_a) end + def test_with_index_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + assert_equal([[1, 0], [2, 1], [3, 2]], @obj.to_enum(:foo, 1, 2, 3).with_index.to_a) + assert_equal([[1, 5], [2, 6], [3, 7]], @obj.to_enum(:foo, 1, 2, 3).with_index(5).to_a) + + s = 1 << (8 * 1.size - 2) + assert_equal([[1, s], [2, s + 1], [3, s + 2]], @obj.to_enum(:foo, 1, 2, 3).with_index(s).to_a) + end + end + def test_with_index_large_offset bug8010 = '[ruby-dev:47131] [Bug #8010]' s = 1 << (8*1.size-2) @@ -244,6 +255,26 @@ class TestEnumerator < Test::Unit::TestCase assert_equal(res, exc.result) end + def test_stopiteration_rescue + e = [1].each + res = e.each {} + e.next + exc0 = assert_raise(StopIteration) { e.peek } + assert_include(exc0.backtrace.first, "test_enumerator.rb:#{__LINE__-1}:") + assert_nil(exc0.cause) + assert_equal(res, exc0.result) + + exc1 = assert_raise(StopIteration) { e.next } + assert_include(exc1.backtrace.first, "test_enumerator.rb:#{__LINE__-1}:") + assert_same(exc0, exc1.cause) + assert_equal(res, exc1.result) + + exc2 = assert_raise(StopIteration) { e.next } + assert_include(exc2.backtrace.first, "test_enumerator.rb:#{__LINE__-1}:") + assert_same(exc0, exc2.cause) + assert_equal(res, exc2.result) + end + def test_next_values o = Object.new def o.each @@ -832,6 +863,21 @@ class TestEnumerator < Test::Unit::TestCase assert_equal(33, chain.next) end + def test_lazy_chain_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + ea = (10..).lazy.select(&:even?).take(10) + ed = (20..).lazy.select(&:odd?) + chain = (ea + ed).select{|x| x % 3 == 0} + assert_equal(12, chain.next) + assert_equal(18, chain.next) + assert_equal(24, chain.next) + assert_equal(21, chain.next) + assert_equal(27, chain.next) + assert_equal(33, chain.next) + end + end + def test_chain_undef_methods chain = [1].to_enum + [2].to_enum meths = (chain.methods & [:feed, :next, :next_values, :peek, :peek_values]) @@ -840,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 = [] @@ -857,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 } @@ -889,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 @@ -907,11 +965,7 @@ class TestEnumerator < Test::Unit::TestCase assert_equal(true, e.is_lambda) end - def test_product - ## - ## Enumerator::Product - ## - + def test_product_new # 0-dimensional e = Enumerator::Product.new assert_instance_of(Enumerator::Product, e) @@ -948,15 +1002,16 @@ class TestEnumerator < Test::Unit::TestCase e.each { |x,| heads << x } assert_equal [1, 1, 2, 2, 3, 3], heads + # Any enumerable is 0 size + assert_equal(0, Enumerator::Product.new([], 1..).size) + # Reject keyword arguments assert_raise(ArgumentError) { Enumerator::Product.new(1..3, foo: 1, bar: 2) } + end - ## - ## Enumerator.product - ## - + def test_s_product # without a block e = Enumerator.product(1..3, %w[a b]) assert_instance_of(Enumerator::Product, e) @@ -983,9 +1038,36 @@ class TestEnumerator < Test::Unit::TestCase assert_equal(nil, e.size) assert_equal [[1, "a"], [1, "b"], [2, "a"], [2, "b"]], e.take(4) + assert_equal(0, Enumerator.product([], 1..).size) + # Reject keyword arguments assert_raise(ArgumentError) { Enumerator.product(1..3, foo: 1, bar: 2) } end + + def test_freeze + e = 3.times.freeze + assert_raise(FrozenError) { e.next } + assert_raise(FrozenError) { e.next_values } + assert_raise(FrozenError) { e.peek } + assert_raise(FrozenError) { e.peek_values } + assert_raise(FrozenError) { e.feed 1 } + assert_raise(FrozenError) { e.rewind } + end + + def test_sum_of_numeric + num = Class.new(Numeric) do + attr_reader :to_f + def initialize(val) + @to_f = Float(val) + end + end + + ary = [5, 10, 20].map {|i| num.new(i)} + + assert_equal(35.0, ary.sum) + enum = ary.each + assert_equal(35.0, enum.sum) + end end diff --git a/test/ruby/test_env.rb b/test/ruby/test_env.rb index cdadeac148..d17e300bce 100644 --- a/test/ruby/test_env.rb +++ b/test/ruby/test_env.rb @@ -2,7 +2,9 @@ require 'test/unit' class TestEnv < Test::Unit::TestCase - IGNORE_CASE = /bccwin|mswin|mingw/ =~ RUBY_PLATFORM + windows = /bccwin|mswin|mingw/ =~ RUBY_PLATFORM + IGNORE_CASE = windows + ENCODING = windows ? Encoding::UTF_8 : Encoding.find("locale") PATH_ENV = "PATH" INVALID_ENVVARS = [ "foo\0bar", @@ -345,12 +347,23 @@ class TestEnv < Test::Unit::TestCase ENV["foo"] = "bar" ENV["baz"] = "qux" s = ENV.inspect + expected = [%("foo" => "bar"), %("baz" => "qux")] + unless s.start_with?(/\{"foo"/i) + expected.reverse! + end + expected = '{' + expected.join(', ') + '}' if IGNORE_CASE s = s.upcase - assert(s == '{"FOO"=>"BAR", "BAZ"=>"QUX"}' || s == '{"BAZ"=>"QUX", "FOO"=>"BAR"}') - else - assert(s == '{"foo"=>"bar", "baz"=>"qux"}' || s == '{"baz"=>"qux", "foo"=>"bar"}') + expected = expected.upcase end + assert_equal(expected, s) + end + + def test_inspect_encoding + ENV.clear + key = "VAR\u{e5 e1 e2 e4 e3 101 3042}" + ENV[key] = "foo" + assert_equal(%{{#{(key.encode(ENCODING) rescue key.b).inspect} => "foo"}}, ENV.inspect) end def test_to_a @@ -359,12 +372,7 @@ class TestEnv < Test::Unit::TestCase ENV["baz"] = "qux" a = ENV.to_a assert_equal(2, a.size) - if IGNORE_CASE - a = a.map {|x| x.map {|y| y.upcase } } - assert(a == [%w(FOO BAR), %w(BAZ QUX)] || a == [%w(BAZ QUX), %w(FOO BAR)]) - else - assert(a == [%w(foo bar), %w(baz qux)] || a == [%w(baz qux), %w(foo bar)]) - end + check([%w(baz qux), %w(foo bar)], a) end def test_rehash @@ -403,8 +411,7 @@ class TestEnv < Test::Unit::TestCase assert_equal("foo", v) end assert_invalid_env {|var| ENV.assoc(var)} - encoding = /mswin|mingw/ =~ RUBY_PLATFORM ? Encoding::UTF_8 : Encoding.find("locale") - assert_equal(encoding, v.encoding) + assert_equal(ENCODING, v.encoding) end def test_has_value2 @@ -450,13 +457,14 @@ class TestEnv < Test::Unit::TestCase assert_equal(h1, h2) end - def check(as, bs) + def assert_equal_env(as, bs) if IGNORE_CASE as = as.map {|k, v| [k.upcase, v] } bs = bs.map {|k, v| [k.upcase, v] } end assert_equal(as.sort, bs.sort) end + alias check assert_equal_env def test_shift ENV.clear @@ -517,7 +525,7 @@ class TestEnv < Test::Unit::TestCase assert_equal(huge_value, ENV["foo"]) end - if /mswin|mingw/ =~ RUBY_PLATFORM + if windows def windows_version @windows_version ||= %x[ver][/Version (\d+)/, 1].to_i end @@ -593,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 @@ -612,8 +620,8 @@ class TestEnv < Test::Unit::TestCase <<-"end;" envvars_to_check = [ "foo\0bar", - "#{'\xa1\xa1'}".force_encoding(Encoding::UTF_16LE), - "foo".force_encoding(Encoding::ISO_2022_JP), + "#{'\xa1\xa1'}".dup.force_encoding(Encoding::UTF_16LE), + "foo".dup.force_encoding(Encoding::ISO_2022_JP), ] envvars_to_check.each do |#{var_name}| #{str_for_yielding_exception_class(code_str)} @@ -641,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; @@ -742,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; @@ -831,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; @@ -843,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; @@ -855,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; @@ -867,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 @@ -880,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) @@ -1002,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) @@ -1024,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 @@ -1075,46 +1084,51 @@ 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 = port.receive + expected = ['"foo" => "bar"', '"baz" => "qux"'] + unless s.start_with?(/\{"foo"/i) + expected.reverse! end - s = r.take + expected = "{" + expected.join(', ') + "}" if #{ignore_case_str} s = s.upcase - assert(s == '{"FOO"=>"BAR", "BAZ"=>"QUX"}' || s == '{"BAZ"=>"QUX", "FOO"=>"BAR"}') - else - assert(s == '{"foo"=>"bar", "baz"=>"qux"}' || s == '{"baz"=>"qux", "foo"=>"bar"}') + expected = expected.upcase end + assert_equal(expected, s) end; end 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} - a = a.map {|x| x.map {|y| y.upcase } } - assert(a == [%w(FOO BAR), %w(BAZ QUX)] || a == [%w(BAZ QUX), %w(FOO BAR)]) - else - assert(a == [%w(foo bar), %w(baz qux)] || a == [%w(baz qux), %w(foo bar)]) + a = a.map {|x, y| [x.upcase, y]} + expected.map! {|x, y| [x.upcase, y]} end + a.sort! + assert_equal(expected, a) end; end @@ -1123,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) @@ -1183,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; @@ -1191,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) @@ -1226,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 @@ -1266,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; @@ -1353,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}"] @@ -1398,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; @@ -1416,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 } @@ -1424,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" } @@ -1432,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 @@ -1479,11 +1494,18 @@ class TestEnv < Test::Unit::TestCase def test_utf8 text = "testing \u{e5 e1 e2 e4 e3 101 3042}" - test = ENV["test"] ENV["test"] = text assert_equal text, ENV["test"] - ensure - ENV["test"] = test + end + + def test_utf8_empty + key = "VAR\u{e5 e1 e2 e4 e3 101 3042}" + ENV[key] = "x" + assert_equal "x", ENV[key] + ENV[key] = "" + assert_equal "", ENV[key] + ENV[key] = nil + assert_nil ENV[key] end end end diff --git a/test/ruby/test_eval.rb b/test/ruby/test_eval.rb index af255c05c8..d2145bec5d 100644 --- a/test/ruby/test_eval.rb +++ b/test/ruby/test_eval.rb @@ -535,6 +535,12 @@ class TestEval < Test::Unit::TestCase assert_equal(fname, eval("__FILE__", nil, fname, 1)) end + def test_eval_invalid_block_exit_bug_20597 + assert_raise(SyntaxError){eval("break if false")} + assert_raise(SyntaxError){eval("next if false")} + assert_raise(SyntaxError){eval("redo if false")} + end + def test_eval_location_fstring o = Object.new o.instance_eval "def foo() end", "generated code" @@ -547,8 +553,8 @@ class TestEval < Test::Unit::TestCase end def test_eval_location_binding - assert_equal(['(eval)', 1], eval("[__FILE__, __LINE__]", nil)) - assert_equal(['(eval)', 1], eval("[__FILE__, __LINE__]", binding)) + assert_equal(["(eval at #{__FILE__}:#{__LINE__})", 1], eval("[__FILE__, __LINE__]", nil)) + assert_equal(["(eval at #{__FILE__}:#{__LINE__})", 1], eval("[__FILE__, __LINE__]", binding)) assert_equal(['foo', 1], eval("[__FILE__, __LINE__]", nil, 'foo')) assert_equal(['foo', 1], eval("[__FILE__, __LINE__]", binding, 'foo')) assert_equal(['foo', 2], eval("[__FILE__, __LINE__]", nil, 'foo', 2)) @@ -606,4 +612,44 @@ class TestEval < Test::Unit::TestCase x = orphan_lambda assert_equal(:ok, x.call) end + + def test_syntax_error_no_memory_leak + assert_no_memory_leak([], "#{<<~'begin;'}", "#{<<~'end;'}", rss: true) + begin; + 100_000.times do + eval("/[/=~s") + rescue SyntaxError + else + raise "Expected SyntaxError to be raised" + end + end; + + assert_no_memory_leak([], "#{<<~'begin;'}", "#{<<~'end;'}", rss: true) + begin; + a = 1 + + 100_000.times do + eval("if a in [0, 0] | [0, a]; end") + rescue SyntaxError + else + raise "Expected SyntaxError to be raised" + end + end; + end + + def test_outer_local_variable_under_gc_compact_stress + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + omit "compaction is not supported on s390x" if /s390x/ =~ RUBY_PLATFORM + + assert_separately([], <<~RUBY) + o = Object.new + def o.m = 1 + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + EnvUtil.under_gc_compact_stress do + assert_equal(1, eval("o.m")) + end + RUBY + end end diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index 51abfee18a..31e5aa9f6b 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -416,7 +416,7 @@ class TestException < Test::Unit::TestCase assert_in_out_err([], "$@ = 1", [], /\$! not set \(ArgumentError\)$/) - assert_in_out_err([], <<-INPUT, [], /backtrace must be Array of String \(TypeError\)$/) + assert_in_out_err([], <<-INPUT, [], /backtrace must be an Array of String or an Array of Thread::Backtrace::Location \(TypeError\)$/) begin raise rescue @@ -508,6 +508,16 @@ end.join assert_raise(TypeError) { e.set_backtrace(1) } assert_raise(TypeError) { e.set_backtrace([1]) } + + error = assert_raise(TypeError) do + e.set_backtrace(caller_locations(1, 1) + ["foo"]) + end + assert_include error.message, "backtrace must be an Array of String or an Array of Thread::Backtrace::Location" + + error = assert_raise(TypeError) do + e.set_backtrace(["foo"] + caller_locations(1, 1)) + end + assert_include error.message, "backtrace must be an Array of String or an Array of Thread::Backtrace::Location" end def test_exit_success_p @@ -540,6 +550,14 @@ end.join assert_equal(Encoding.find("locale"), Errno::EINVAL.new.message.encoding) end + def test_errno_constants + assert_equal [:NOERROR], Errno.constants.grep_v(/\AE/) + all_assertions_foreach("should be a subclass of SystemCallError", *Errno.constants) do |c| + e = Errno.const_get(c) + assert_operator e, :<, SystemCallError, proc {e.ancestors.inspect} + end + end + def test_too_many_args_in_eval bug5720 = '[ruby-core:41520]' arg_string = (0...140000).to_a.join(", ") @@ -685,7 +703,7 @@ end.join def test_machine_stackoverflow bug9109 = '[ruby-dev:47804] [Bug #9109]' - assert_separately(%w[--disable-gem], <<-SRC) + assert_separately([], <<-SRC) assert_raise(SystemStackError, #{bug9109.dump}) { h = {a: ->{h[:a].call}} h[:a].call @@ -696,7 +714,7 @@ end.join def test_machine_stackoverflow_by_define_method bug9454 = '[ruby-core:60113] [Bug #9454]' - assert_separately(%w[--disable-gem], <<-SRC) + assert_separately([], <<-SRC) assert_raise(SystemStackError, #{bug9454.dump}) { define_method(:foo) {self.foo} self.foo @@ -801,7 +819,7 @@ end.join def test_cause_at_end errs = [ /-: unexpected return\n/, - /.*undefined local variable or method `n'.*\n/, + /.*undefined local variable or method 'n'.*\n/, ] assert_in_out_err([], <<-'end;', [], errs) END{n}; END{return} @@ -974,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 @@ -1037,7 +1055,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| end def test_message_of_name_error - assert_raise_with_message(NameError, /\Aundefined method `foo' for module `#<Module:.*>'$/) do + assert_raise_with_message(NameError, /\Aundefined method 'foo' for module '#<Module:.*>'$/) do Module.new do module_function :foo end @@ -1046,8 +1064,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| def capture_warning_warn(category: false) verbose = $VERBOSE - deprecated = Warning[:deprecated] - experimental = Warning[:experimental] + categories = Warning.categories.to_h {|cat| [cat, Warning[cat]]} warning = [] ::Warning.class_eval do @@ -1066,15 +1083,13 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| end $VERBOSE = true - Warning[:deprecated] = true - Warning[:experimental] = true + Warning.categories.each {|cat| Warning[cat] = true} yield return warning ensure $VERBOSE = verbose - Warning[:deprecated] = deprecated - Warning[:experimental] = experimental + categories.each {|cat, flag| Warning[cat] = flag} ::Warning.class_eval do remove_method :warn @@ -1085,7 +1100,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| def test_warning_warn warning = capture_warning_warn {$asdfasdsda_test_warning_warn} - assert_match(/global variable `\$asdfasdsda_test_warning_warn' not initialized/, warning[0]) + assert_match(/global variable '\$asdfasdsda_test_warning_warn' not initialized/, warning[0]) assert_equal(["a\nz\n"], capture_warning_warn {warn "a\n", "z"}) assert_equal([], capture_warning_warn {warn}) @@ -1093,19 +1108,13 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| end def test_warn_deprecated_backwards_compatibility_category - omit "no method to test" - - warning = capture_warning_warn { } - - assert_match(/deprecated/, warning[0]) - end - - def test_warn_deprecated_category - omit "no method to test" - - warning = capture_warning_warn(category: true) { } + (message, category), = capture_warning_warn(category: true) do + $; = "www" + $; = nil + end - assert_equal :deprecated, warning[0][1] + assert_include message, 'deprecated' + assert_equal :deprecated, category end def test_kernel_warn_uplevel @@ -1119,11 +1128,11 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| assert_raise(ArgumentError) {warn("test warning", uplevel: -1)} assert_in_out_err(["-e", "warn 'ok', uplevel: 1"], '', [], /warning:/) warning = capture_warning_warn {warn("test warning", {uplevel: 0})} - assert_match(/test warning.*{:uplevel=>0}/m, warning[0]) + assert_match(/test warning.*{uplevel: 0}/m, warning[0]) warning = capture_warning_warn {warn("test warning", **{uplevel: 0})} assert_equal("#{__FILE__}:#{__LINE__-1}: warning: test warning\n", warning[0]) warning = capture_warning_warn {warn("test warning", {uplevel: 0}, **{})} - assert_equal("test warning\n{:uplevel=>0}\n", warning[0]) + assert_equal("test warning\n{uplevel: 0}\n", warning[0]) assert_raise(ArgumentError) {warn("test warning", foo: 1)} end @@ -1161,7 +1170,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| end def test_warning_warn_super - assert_in_out_err(%[-W0], "#{<<~"{#"}\n#{<<~'};'}", [], /global variable `\$asdfiasdofa_test_warning_warn_super' not initialized/) + assert_in_out_err(%[-W0], "#{<<~"{#"}\n#{<<~'};'}", [], /global variable '\$asdfiasdofa_test_warning_warn_super' not initialized/) {# module Warning def warn(message) @@ -1177,48 +1186,24 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| def test_warning_category assert_raise(TypeError) {Warning[nil]} assert_raise(ArgumentError) {Warning[:XXXX]} - assert_include([true, false], Warning[:deprecated]) - assert_include([true, false], Warning[:experimental]) - end - - def test_warning_category_deprecated - warning = EnvUtil.verbose_warning do - deprecated = Warning[:deprecated] - Warning[:deprecated] = true - Warning.warn "deprecated feature", category: :deprecated - ensure - Warning[:deprecated] = deprecated - end - assert_equal "deprecated feature", warning - - warning = EnvUtil.verbose_warning do - deprecated = Warning[:deprecated] - Warning[:deprecated] = false - Warning.warn "deprecated feature", category: :deprecated - ensure - Warning[:deprecated] = deprecated - end - assert_empty warning - end - def test_warning_category_experimental - warning = EnvUtil.verbose_warning do - experimental = Warning[:experimental] - Warning[:experimental] = true - Warning.warn "experimental feature", category: :experimental - ensure - Warning[:experimental] = experimental - end - assert_equal "experimental feature", warning + all_assertions_foreach("categories", *Warning.categories) do |cat| + value = Warning[cat] + assert_include([true, false], value) - warning = EnvUtil.verbose_warning do - experimental = Warning[:experimental] - Warning[:experimental] = false - Warning.warn "experimental feature", category: :experimental + enabled = EnvUtil.verbose_warning do + Warning[cat] = true + Warning.warn "#{cat} feature", category: cat + end + disabled = EnvUtil.verbose_warning do + Warning[cat] = false + Warning.warn "#{cat} feature", category: cat + end ensure - Warning[:experimental] = experimental + Warning[cat] = value + assert_equal "#{cat} feature", enabled + assert_empty disabled end - assert_empty warning end def test_undef_Warning_warn @@ -1310,7 +1295,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| def test_backtrace_in_eval bug = '[ruby-core:84434] [Bug #14229]' - assert_in_out_err(['-e', 'eval("raise")'], "", [], /^\(eval\):1:/, bug) + assert_in_out_err(['-e', 'eval("raise")'], "", [], /^\(eval at .*\):1:/, bug) end def test_full_message @@ -1413,11 +1398,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| end def test_marshal_circular_cause - begin - raise RuntimeError, "err", [], cause: Exception.new - rescue => e - end - dump = Marshal.dump(e).sub(/o:\x0EException\x08;.0;.0;.0/, "@\x05") + dump = "\x04\bo:\x11RuntimeError\b:\tmesgI\"\berr\x06:\x06ET:\abt[\x00:\ncause@\x05" assert_raise_with_message(ArgumentError, /circular cause/, ->{dump.inspect}) do e = Marshal.load(dump) assert_same(e, e.cause) @@ -1435,7 +1416,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| end bug14670 = '[ruby-dev:50522] [Bug #14670]' - assert_raise_with_message(NoMethodError, /`foo'/, bug14670) do + assert_raise_with_message(NoMethodError, /'foo'/, bug14670) do Object.new.foo end end; @@ -1459,6 +1440,15 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| assert_equal("\e[1mRuntimeError (\e[1;4mRuntimeError\e[m\e[1m)\e[m", e.detailed_message(highlight: true)) end + def test_detailed_message_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + e = RuntimeError.new("foo\nbar\nbaz") + assert_equal("foo (RuntimeError)\nbar\nbaz", e.detailed_message) + assert_equal("\e[1mfoo (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n\e[1mbar\e[m\n\e[1mbaz\e[m", e.detailed_message(highlight: true)) + end + end + def test_full_message_with_custom_detailed_message e = RuntimeError.new("message") opt_ = nil @@ -1470,6 +1460,19 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| assert_equal({ highlight: Exception.to_tty? }, opt_) end + def test_full_message_with_encoding + message = "\u{dc}bersicht" + begin + begin + raise message + rescue => e + raise "\n#{e.message}" + end + rescue => e + end + assert_include(e.full_message, message) + end + def test_syntax_error_detailed_message Dir.mktmpdir do |dir| File.write(File.join(dir, "detail.rb"), "#{<<~"begin;"}\n#{<<~'end;'}") @@ -1495,7 +1498,7 @@ $stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| assert_not_empty(stderr.grep(pattern)) error, = stderr.grep(/unexpected end-of-input/) assert_not_nil(error) - assert_match(/<.*unexpected end-of-input.*>/, error) + assert_match(/<.*unexpected end-of-input.*>|\^ unexpected end-of-input,/, error) end end end @@ -1522,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 cb6e846bc6..b7d2b71c19 100644 --- a/test/ruby/test_fiber.rb +++ b/test/ruby/test_fiber.rb @@ -34,7 +34,6 @@ class TestFiber < Test::Unit::TestCase end def test_many_fibers - omit 'This is unstable on GitHub Actions --jit-wait. TODO: debug it' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? max = 1000 assert_equal(max, max.times{ Fiber.new{} @@ -50,7 +49,7 @@ class TestFiber < Test::Unit::TestCase end def test_many_fibers_with_threads - assert_normal_exit <<-SRC, timeout: (/solaris/i =~ RUBY_PLATFORM ? 1000 : 60) + assert_normal_exit <<-SRC, timeout: 60 max = 1000 @cnt = 0 (1..100).map{|ti| @@ -82,12 +81,14 @@ class TestFiber < Test::Unit::TestCase f.resume f.resume } - assert_raise(RuntimeError){ - Fiber.new{ - @c = callcc{|c| @c = c} - }.resume - @c.call # cross fiber callcc - } + if respond_to?(:callcc) + assert_raise(RuntimeError){ + Fiber.new{ + @c = callcc{|c| @c = c} + }.resume + @c.call # cross fiber callcc + } + end assert_raise(RuntimeError){ Fiber.new{ raise @@ -250,6 +251,18 @@ class TestFiber < Test::Unit::TestCase assert_equal(nil, Thread.current[:v]); end + def test_fiber_variables + assert_equal "bar", Fiber.new {Fiber[:foo] = "bar"; Fiber[:foo]}.resume + + key = :"#{self.class.name}#.#{self.object_id}" + Fiber[key] = 42 + assert_equal 42, Fiber[key] + + key = Object.new + def key.to_str; "foo"; end + assert_equal "Bar", Fiber.new {Fiber[key] = "Bar"; Fiber[key]}.resume + end + def test_alive fib = Fiber.new{Fiber.yield} assert_equal(true, fib.alive?) @@ -422,7 +435,7 @@ class TestFiber < Test::Unit::TestCase end def test_fatal_in_fiber - assert_in_out_err(["-r-test-/fatal/rb_fatal", "-e", <<-EOS], "", [], /ok/) + assert_in_out_err(["-r-test-/fatal", "-e", <<-EOS], "", [], /ok/) Fiber.new{ Bug.rb_fatal "ok" }.resume @@ -485,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 409d21fc4e..a3d6221c0f 100644 --- a/test/ruby/test_file.rb +++ b/test/ruby/test_file.rb @@ -358,33 +358,58 @@ class TestFile < Test::Unit::TestCase assert_equal(mod_time_contents, stats.mtime, bug6385) end + def measure_time + log = [] + 30.times do + t1 = Process.clock_gettime(Process::CLOCK_REALTIME) + yield + t2 = Process.clock_gettime(Process::CLOCK_REALTIME) + log << (t2 - t1) + return (t1 + t2) / 2 if t2 - t1 < 1 + sleep 1 + end + omit "failed to setup; the machine is stupidly slow #{log.inspect}" + 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 - t0 = Process.clock_gettime(Process::CLOCK_REALTIME) - File.write(path, "foo") + measure_time do + File.write(path, "foo") + end + sleep 2 - File.write(path, "bar") + + mtime = measure_time do + File.write(path, "bar") + end + sleep 2 - File.read(path) - File.chmod(0644, path) + + ctime = measure_time do + File.chmod(0644, path) + end + sleep 2 - File.read(path) + + 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 t0+2, 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 t0+4, 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 t0+6, stat.atime.to_f, delta + assert_in_delta atime, stat.atime.to_f, delta end } rescue NotImplementedError @@ -460,6 +485,39 @@ class TestFile < Test::Unit::TestCase end end + def test_initialize + Dir.mktmpdir(__method__.to_s) do |tmpdir| + path = File.join(tmpdir, "foo") + + assert_raise(Errno::ENOENT) {File.new(path)} + f = File.new(path, "w") + f.write("FOO\n") + f.close + f = File.new(path) + data = f.read + f.close + assert_equal("FOO\n", data) + + f = File.new(path, File::WRONLY) + f.write("BAR\n") + f.close + f = File.new(path, File::RDONLY) + data = f.read + f.close + assert_equal("BAR\n", data) + + data = File.open(path) {|file| + File.new(file.fileno, mode: File::RDONLY, autoclose: false).read + } + assert_equal("BAR\n", data) + + data = File.open(path) {|file| + File.new(file.fileno, File::RDONLY, autoclose: false).read + } + assert_equal("BAR\n", data) + end + end + def test_file_open_newline_option Dir.mktmpdir(__method__.to_s) do |tmpdir| path = File.join(tmpdir, "foo") @@ -554,4 +612,250 @@ class TestFile < Test::Unit::TestCase assert_file.absolute_path?("/foo/bar\\baz") end end + + class NewlineConvTests < Test::Unit::TestCase + TEST_STRING_WITH_CRLF = "line1\r\nline2\r\n".freeze + TEST_STRING_WITH_LF = "line1\nline2\n".freeze + + def setup + @tmpdir = Dir.mktmpdir(self.class.name) + @read_path_with_crlf = File.join(@tmpdir, "read_path_with_crlf") + File.binwrite(@read_path_with_crlf, TEST_STRING_WITH_CRLF) + @read_path_with_lf = File.join(@tmpdir, "read_path_with_lf") + File.binwrite(@read_path_with_lf, TEST_STRING_WITH_LF) + @write_path = File.join(@tmpdir, "write_path") + File.binwrite(@write_path, '') + end + + def teardown + FileUtils.rm_rf @tmpdir + end + + def windows? + /cygwin|mswin|mingw/ =~ RUBY_PLATFORM + end + + def open_file_with(method, filename, mode) + read_or_write = mode.include?('w') ? :write : :read + binary_or_text = mode.include?('b') ? :binary : :text + + f = case method + when :ruby_file_open + File.open(filename, mode) + when :c_rb_file_open + Bug::File::NewlineConv.rb_file_open(filename, read_or_write, binary_or_text) + when :c_rb_io_fdopen + Bug::File::NewlineConv.rb_io_fdopen(filename, read_or_write, binary_or_text) + else + raise "Don't know how to open with #{method}" + end + + begin + yield f + ensure + f.close + end + end + + def assert_file_contents_has_lf(f) + assert_equal TEST_STRING_WITH_LF, f.read + end + + def assert_file_contents_has_crlf(f) + assert_equal TEST_STRING_WITH_CRLF, f.read + end + + def assert_file_contents_has_lf_on_windows(f) + if windows? + assert_file_contents_has_lf(f) + else + assert_file_contents_has_crlf(f) + end + end + + def assert_file_contents_has_crlf_on_windows(f) + if windows? + assert_file_contents_has_crlf(f) + else + assert_file_contents_has_lf(f) + end + end + + def test_ruby_file_open_text_mode_read_crlf + open_file_with(:ruby_file_open, @read_path_with_crlf, 'r') { |f| assert_file_contents_has_lf_on_windows(f) } + end + + def test_ruby_file_open_bin_mode_read_crlf + open_file_with(:ruby_file_open, @read_path_with_crlf, 'rb') { |f| assert_file_contents_has_crlf(f) } + end + + def test_ruby_file_open_text_mode_read_lf + open_file_with(:ruby_file_open, @read_path_with_lf, 'r') { |f| assert_file_contents_has_lf(f) } + end + + def test_ruby_file_open_bin_mode_read_lf + open_file_with(:ruby_file_open, @read_path_with_lf, 'rb') { |f| assert_file_contents_has_lf(f) } + end + + def test_ruby_file_open_text_mode_read_crlf_with_utf8_encoding + open_file_with(:ruby_file_open, @read_path_with_crlf, 'r') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf_on_windows(f) + end + end + + def test_ruby_file_open_bin_mode_read_crlf_with_utf8_encoding + open_file_with(:ruby_file_open, @read_path_with_crlf, 'rb') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_crlf(f) + end + end + + def test_ruby_file_open_text_mode_read_lf_with_utf8_encoding + open_file_with(:ruby_file_open, @read_path_with_lf, 'r') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf(f) + end + end + + def test_ruby_file_open_bin_mode_read_lf_with_utf8_encoding + open_file_with(:ruby_file_open, @read_path_with_lf, 'rb') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf(f) + end + end + + def test_ruby_file_open_text_mode_write_lf + open_file_with(:ruby_file_open, @write_path, 'w') { |f| f.write TEST_STRING_WITH_LF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_crlf_on_windows(f) } + end + + def test_ruby_file_open_bin_mode_write_lf + open_file_with(:ruby_file_open, @write_path, 'wb') { |f| f.write TEST_STRING_WITH_LF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_lf(f) } + end + + def test_ruby_file_open_bin_mode_write_crlf + open_file_with(:ruby_file_open, @write_path, 'wb') { |f| f.write TEST_STRING_WITH_CRLF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_crlf(f) } + end + + def test_c_rb_file_open_text_mode_read_crlf + open_file_with(:c_rb_file_open, @read_path_with_crlf, 'r') { |f| assert_file_contents_has_lf_on_windows(f) } + end + + def test_c_rb_file_open_bin_mode_read_crlf + open_file_with(:c_rb_file_open, @read_path_with_crlf, 'rb') { |f| assert_file_contents_has_crlf(f) } + end + + def test_c_rb_file_open_text_mode_read_lf + open_file_with(:c_rb_file_open, @read_path_with_lf, 'r') { |f| assert_file_contents_has_lf(f) } + end + + def test_c_rb_file_open_bin_mode_read_lf + open_file_with(:c_rb_file_open, @read_path_with_lf, 'rb') { |f| assert_file_contents_has_lf(f) } + end + + def test_c_rb_file_open_text_mode_write_lf + open_file_with(:c_rb_file_open, @write_path, 'w') { |f| f.write TEST_STRING_WITH_LF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_crlf_on_windows(f) } + end + + def test_c_rb_file_open_bin_mode_write_lf + open_file_with(:c_rb_file_open, @write_path, 'wb') { |f| f.write TEST_STRING_WITH_LF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_lf(f) } + end + + def test_c_rb_file_open_bin_mode_write_crlf + open_file_with(:c_rb_file_open, @write_path, 'wb') { |f| f.write TEST_STRING_WITH_CRLF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_crlf(f) } + end + + def test_c_rb_file_open_text_mode_read_crlf_with_utf8_encoding + open_file_with(:c_rb_file_open, @read_path_with_crlf, 'r') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf_on_windows(f) + end + end + + def test_c_rb_file_open_bin_mode_read_crlf_with_utf8_encoding + open_file_with(:c_rb_file_open, @read_path_with_crlf, 'rb') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_crlf(f) + end + end + + def test_c_rb_file_open_text_mode_read_lf_with_utf8_encoding + open_file_with(:c_rb_file_open, @read_path_with_lf, 'r') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf(f) + end + end + + def test_c_rb_file_open_bin_mode_read_lf_with_utf8_encoding + open_file_with(:c_rb_file_open, @read_path_with_lf, 'rb') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf(f) + end + end + + def test_c_rb_io_fdopen_text_mode_read_crlf + open_file_with(:c_rb_io_fdopen, @read_path_with_crlf, 'r') { |f| assert_file_contents_has_lf_on_windows(f) } + end + + def test_c_rb_io_fdopen_bin_mode_read_crlf + open_file_with(:c_rb_io_fdopen, @read_path_with_crlf, 'rb') { |f| assert_file_contents_has_crlf(f) } + end + + def test_c_rb_io_fdopen_text_mode_read_lf + open_file_with(:c_rb_io_fdopen, @read_path_with_lf, 'r') { |f| assert_file_contents_has_lf(f) } + end + + def test_c_rb_io_fdopen_bin_mode_read_lf + open_file_with(:c_rb_io_fdopen, @read_path_with_lf, 'rb') { |f| assert_file_contents_has_lf(f) } + end + + def test_c_rb_io_fdopen_text_mode_write_lf + open_file_with(:c_rb_io_fdopen, @write_path, 'w') { |f| f.write TEST_STRING_WITH_LF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_crlf_on_windows(f) } + end + + def test_c_rb_io_fdopen_bin_mode_write_lf + open_file_with(:c_rb_io_fdopen, @write_path, 'wb') { |f| f.write TEST_STRING_WITH_LF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_lf(f) } + end + + def test_c_rb_io_fdopen_bin_mode_write_crlf + open_file_with(:c_rb_io_fdopen, @write_path, 'wb') { |f| f.write TEST_STRING_WITH_CRLF } + File.open(@write_path, 'rb') { |f| assert_file_contents_has_crlf(f) } + end + + def test_c_rb_io_fdopen_text_mode_read_crlf_with_utf8_encoding + open_file_with(:c_rb_io_fdopen, @read_path_with_crlf, 'r') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf_on_windows(f) + end + end + + def test_c_rb_io_fdopen_bin_mode_read_crlf_with_utf8_encoding + open_file_with(:c_rb_io_fdopen, @read_path_with_crlf, 'rb') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_crlf(f) + end + end + + def test_c_rb_io_fdopen_text_mode_read_lf_with_utf8_encoding + open_file_with(:c_rb_io_fdopen, @read_path_with_lf, 'r') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf(f) + end + end + + def test_c_rb_io_fdopen_bin_mode_read_lf_with_utf8_encoding + open_file_with(:c_rb_io_fdopen, @read_path_with_lf, 'rb') do |f| + f.set_encoding Encoding::UTF_8, '-' + assert_file_contents_has_lf(f) + end + end + end end diff --git a/test/ruby/test_file_exhaustive.rb b/test/ruby/test_file_exhaustive.rb index be6e1f2326..394dc47603 100644 --- a/test/ruby/test_file_exhaustive.rb +++ b/test/ruby/test_file_exhaustive.rb @@ -6,7 +6,8 @@ require "socket" require '-test-/file' class TestFileExhaustive < Test::Unit::TestCase - DRIVE = Dir.pwd[%r'\A(?:[a-z]:|//[^/]+/[^/]+)'i] + ROOT_REGEXP = %r'\A(?:[a-z]:(?=(/))|//[^/]+/[^/]+)'i + DRIVE = Dir.pwd[ROOT_REGEXP] POSIX = /cygwin|mswin|bccwin|mingw|emx/ !~ RUBY_PLATFORM NTFS = !(/mingw|mswin|bccwin/ !~ RUBY_PLATFORM) @@ -186,16 +187,42 @@ class TestFileExhaustive < Test::Unit::TestCase @blockdev end + def root_without_capabilities? + return false unless Process.uid == 0 + return false unless system('command', '-v', 'capsh', out: File::NULL) + !system('capsh', '--has-p=CAP_DAC_OVERRIDE', out: File::NULL, err: File::NULL) + end + def test_path [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) @@ -1272,9 +1299,10 @@ class TestFileExhaustive < Test::Unit::TestCase assert_equal(regular_file, File.dirname(regular_file, 0)) assert_equal(@dir, File.dirname(regular_file, 1)) assert_equal(File.dirname(@dir), File.dirname(regular_file, 2)) - return if /mswin/ =~ RUBY_PLATFORM && ENV.key?('GITHUB_ACTIONS') # rootdir and tmpdir are in different drives - assert_equal(rootdir, File.dirname(regular_file, regular_file.count('/'))) assert_raise(ArgumentError) {File.dirname(regular_file, -1)} + root = "#{@dir[ROOT_REGEXP]||?/}#{$1}" + assert_equal(root, File.dirname(regular_file, regular_file.count('/'))) + assert_equal(root, File.dirname(regular_file, regular_file.count('/') + 100)) end def test_dirname_encoding @@ -1409,7 +1437,7 @@ class TestFileExhaustive < Test::Unit::TestCase def test_flock_exclusive omit "[Bug #18613]" if /freebsd/ =~ RUBY_PLATFORM - timeout = EnvUtil.apply_timeout_scale(0.1).to_s + timeout = EnvUtil.apply_timeout_scale(1).to_s File.open(regular_file, "r+") do |f| f.flock(File::LOCK_EX) assert_separately(["-rtimeout", "-", regular_file, timeout], "#{<<-"begin;"}\n#{<<-'end;'}") @@ -1440,7 +1468,7 @@ class TestFileExhaustive < Test::Unit::TestCase def test_flock_shared omit "[Bug #18613]" if /freebsd/ =~ RUBY_PLATFORM - timeout = EnvUtil.apply_timeout_scale(0.1).to_s + timeout = EnvUtil.apply_timeout_scale(1).to_s File.open(regular_file, "r+") do |f| f.flock(File::LOCK_SH) assert_separately(["-rtimeout", "-", regular_file, timeout], "#{<<-"begin;"}\n#{<<-'end;'}") @@ -1469,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) @@ -1518,7 +1547,7 @@ class TestFileExhaustive < Test::Unit::TestCase stat = File.stat(f) unless stat.chardev? - # /dev/null may be accessed by other processes + # null device may be accessed by other processes assert_equal(stat.atime, File.atime(f), f) assert_equal(stat.ctime, File.ctime(f), f) assert_equal(stat.mtime, File.mtime(f), f) @@ -1538,8 +1567,17 @@ class TestFileExhaustive < Test::Unit::TestCase assert_equal(stat.size?, File.size?(f), f) assert_bool_equal(stat.socket?, File.socket?(f), f) assert_bool_equal(stat.setuid?, File.setuid?(f), f) - assert_bool_equal(stat.writable?, File.writable?(f), f) - assert_bool_equal(stat.writable_real?, File.writable_real?(f), f) + # It's possible in Linux to be uid 0, but not to have the CAP_DAC_OVERRIDE + # capability that allows skipping file permissions checks (e.g. some kinds + # of "rootless" container setups). In these cases, stat.writable? will be + # true (because it always returns true if Process.uid == 0), but + # File.writeable? will be false (because it actually asks the kernel to do + # an access check). + # Skip these two assertions in that case. + unless root_without_capabilities? + assert_bool_equal(stat.writable?, File.writable?(f), f) + assert_bool_equal(stat.writable_real?, File.writable_real?(f), f) + end assert_bool_equal(stat.executable?, File.executable?(f), f) assert_bool_equal(stat.executable_real?, File.executable_real?(f), f) assert_bool_equal(stat.zero?, File.zero?(f), f) diff --git a/test/ruby/test_float.rb b/test/ruby/test_float.rb index b91b904d1e..d0d180593a 100644 --- a/test/ruby/test_float.rb +++ b/test/ruby/test_float.rb @@ -129,9 +129,9 @@ class TestFloat < Test::Unit::TestCase assert_in_delta(a, 0, Float::EPSILON) a = Float("-.0") assert_in_delta(a, 0, Float::EPSILON) - assert_raise(ArgumentError){Float("0.")} - assert_raise(ArgumentError){Float("+0.")} - assert_raise(ArgumentError){Float("-0.")} + assert_equal(0.0, Float("0.")) + assert_equal(0.0, Float("+0.")) + assert_equal(0.0, Float("-0.")) assert_raise(ArgumentError){Float(".")} assert_raise(ArgumentError){Float("+")} assert_raise(ArgumentError){Float("+.")} @@ -139,12 +139,12 @@ class TestFloat < Test::Unit::TestCase assert_raise(ArgumentError){Float("-.")} assert_raise(ArgumentError){Float("1e")} assert_raise(ArgumentError){Float("1__1")} - assert_raise(ArgumentError){Float("1.")} - assert_raise(ArgumentError){Float("1.e+00")} - assert_raise(ArgumentError){Float("0x.1")} - assert_raise(ArgumentError){Float("0x1.")} - assert_raise(ArgumentError){Float("0x1.0")} - assert_raise(ArgumentError){Float("0x1.p+0")} + assert_equal(1.0, Float("1.")) + assert_equal(1.0, Float("1.e+00")) + assert_equal(0.0625, Float("0x.1")) + assert_equal(1.0, Float("0x1.")) + assert_equal(1.0, Float("0x1.0")) + assert_equal(1.0, Float("0x1.p+0")) # add expected behaviour here. assert_equal(10, Float("1_0")) @@ -191,7 +191,7 @@ class TestFloat < Test::Unit::TestCase break end end - assert_nil(x, ->{"%a" % x}) + assert_equal(1.0, x, ->{"%a" % x}) end def test_divmod @@ -530,6 +530,10 @@ class TestFloat < Test::Unit::TestCase assert_raise(TypeError) {1.0.floor(nil)} def (prec = Object.new).to_int; 2; end assert_equal(0.99, 0.998.floor(prec)) + + assert_equal(-10000000000, -1.0.floor(-10), "[Bug #20654]") + assert_equal(-100000000000000000000, -1.0.floor(-20), "[Bug #20654]") + assert_equal(-100000000000000000000000000000000000000000000000000, -1.0.floor(-50), "[Bug #20654]") end def test_ceil_with_precision @@ -557,6 +561,10 @@ class TestFloat < Test::Unit::TestCase assert_raise(TypeError) {1.0.ceil(nil)} def (prec = Object.new).to_int; 2; end assert_equal(0.99, 0.981.ceil(prec)) + + assert_equal(10000000000, 1.0.ceil(-10), "[Bug #20654]") + assert_equal(100000000000000000000, 1.0.ceil(-20), "[Bug #20654]") + assert_equal(100000000000000000000000000000000000000000000000000, 1.0.ceil(-50), "[Bug #20654]") end def test_truncate_with_precision @@ -825,11 +833,15 @@ class TestFloat < Test::Unit::TestCase assert_equal(15, Float('0xf')) assert_equal(15, Float('0xfp0')) assert_raise(ArgumentError) { Float('0xfp') } - assert_raise(ArgumentError) { Float('0xf.') } + assert_equal(15, Float('0xf.')) assert_raise(ArgumentError) { Float('0xf.p') } - assert_raise(ArgumentError) { Float('0xf.p0') } - assert_raise(ArgumentError) { Float('0xf.f') } + assert_equal(15, Float('0xf.p0')) + assert_equal(15.9375, Float('0xf.f')) assert_raise(ArgumentError) { Float('0xf.fp') } + assert_equal(0x10a, Float("0x1_0a")) + assert_equal(1.625, Float("0x1.a_0")) + assert_equal(3.25, Float("0x1.ap0_1")) + assert_raise(ArgumentError) { Float("0x1.ap0a") } begin verbose_bak, $VERBOSE = $VERBOSE, nil assert_equal(Float::INFINITY, Float('0xf.fp1000000000000000')) @@ -842,6 +854,16 @@ class TestFloat < Test::Unit::TestCase o = Object.new def o.to_f; inf = Float::INFINITY; inf/inf; end assert_predicate(Float(o), :nan?) + + assert_raise(Encoding::CompatibilityError) {Float("0".encode("utf-16be"))} + assert_raise(Encoding::CompatibilityError) {Float("0".encode("utf-16le"))} + assert_raise(Encoding::CompatibilityError) {Float("0".encode("utf-32be"))} + assert_raise(Encoding::CompatibilityError) {Float("0".encode("utf-32le"))} + assert_raise(Encoding::CompatibilityError) {Float("0".encode("iso-2022-jp"))} + + 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 4f1d32580a..09199c34b1 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -40,16 +40,91 @@ class TestGc < Test::Unit::TestCase end def test_enable_disable + EnvUtil.without_gc do + GC.enable + assert_equal(false, GC.enable) + assert_equal(false, GC.disable) + assert_equal(true, GC.disable) + assert_equal(true, GC.disable) + assert_nil(GC.start) + assert_equal(true, GC.enable) + assert_equal(false, GC.enable) + end + end + + def test_gc_config_full_mark_by_default + config = GC.config + assert_not_empty(config) + assert_true(config[:rgengc_allow_full_mark]) + end + + def test_gc_config_invalid_args + assert_raise(ArgumentError) { GC.config(0) } + end + + def test_gc_config_setting_returns_updated_config_hash + old_value = GC.config[:rgengc_allow_full_mark] + assert_true(old_value) + + new_value = GC.config(rgengc_allow_full_mark: false)[:rgengc_allow_full_mark] + assert_false(new_value) + new_value = GC.config(rgengc_allow_full_mark: nil)[:rgengc_allow_full_mark] + assert_false(new_value) + ensure + GC.config(rgengc_allow_full_mark: old_value) + GC.start + end + + 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 GC.enable - assert_equal(false, GC.enable) - assert_equal(false, GC.disable) - assert_equal(true, GC.disable) - assert_equal(true, GC.disable) + GC.start + + GC.config(rgengc_allow_full_mark: false) + major_count = GC.stat[:major_gc_count] + minor_count = GC.stat[:minor_gc_count] + + arr = [] + (GC.stat_heap[0][:heap_eden_slots] * 2).times do + arr << Object.new + Object.new + end + + assert_equal(major_count, GC.stat[:major_gc_count]) + assert_operator(minor_count, :<=, GC.stat[:minor_gc_count]) assert_nil(GC.start) - assert_equal(true, GC.enable) - assert_equal(false, GC.enable) ensure - GC.enable + GC.config(rgengc_allow_full_mark: true) + GC.start + end + + def test_gc_config_disable_major_gc_start_always_works + GC.config(full_mark: false) + + major_count = GC.stat[:major_gc_count] + GC.start + + assert_operator(major_count, :<, GC.stat[:major_gc_count]) + ensure + GC.config(full_mark: true) + GC.start + end + + def test_gc_config_implementation + omit unless /darwin|linux/.match(RUBY_PLATFORM) + + gc_name = (ENV['RUBY_GC_LIBRARY'] || "default") + assert_equal gc_name, GC.config[:implementation] + end + + def test_gc_config_implementation_is_readonly + omit unless /darwin|linux/.match(RUBY_PLATFORM) + + assert_raise(ArgumentError) { GC.config(implementation: "somethingelse") } end def test_start_full_mark @@ -131,10 +206,9 @@ class TestGc < Test::Unit::TestCase # marking_time + sweeping_time could differ from time by 1 because they're stored in nanoseconds assert_in_delta stat[:time], stat[:marking_time] + stat[:sweeping_time], 1 assert_equal stat[:total_allocated_pages], stat[:heap_allocated_pages] + stat[:total_freed_pages] - assert_operator stat[:heap_sorted_length], :>=, stat[:heap_eden_pages] + stat[:heap_allocatable_pages], "stat is: " + stat.inspect assert_equal stat[:heap_available_slots], stat[:heap_live_slots] + stat[:heap_free_slots] + stat[:heap_final_slots] assert_equal stat[:heap_live_slots], stat[:total_allocated_objects] - stat[:total_freed_objects] - stat[:heap_final_slots] - assert_equal stat[:heap_allocated_pages], stat[:heap_eden_pages] + stat[:heap_tomb_pages] + assert_equal stat[:heap_allocated_pages], stat[:heap_eden_pages] + stat[:heap_empty_pages] if use_rgengc? assert_equal stat[:count], stat[:major_gc_count] + stat[:minor_gc_count] @@ -150,19 +224,24 @@ class TestGc < Test::Unit::TestCase GC.stat_heap(0, stat_heap) GC.stat(stat) - GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT].times do |i| - GC.stat_heap(i, stat_heap) - GC.stat(stat) + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| + EnvUtil.without_gc do + GC.stat_heap(i, stat_heap) + GC.stat(stat) + end - assert_equal GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] * (2**i), stat_heap[:slot_size] - assert_operator stat_heap[:heap_allocatable_pages], :<=, stat[:heap_allocatable_pages] + 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[:heap_tomb_pages], :<=, stat[:heap_tomb_pages] - assert_operator stat_heap[:heap_tomb_slots], :>=, 0 assert_operator stat_heap[:total_allocated_pages], :>=, 0 - assert_operator stat_heap[:total_freed_pages], :>=, 0 assert_operator stat_heap[:force_major_gc_count], :>=, 0 + assert_operator stat_heap[:force_incremental_marking_finish_count], :>=, 0 + assert_operator stat_heap[:total_allocated_objects], :>=, 0 + assert_operator stat_heap[:total_freed_objects], :>=, 0 + assert_operator stat_heap[:total_freed_objects], :<=, stat_heap[:total_allocated_objects] end GC.stat_heap(0, stat_heap) @@ -170,21 +249,25 @@ class TestGc < Test::Unit::TestCase assert_equal stat_heap[:slot_size], GC.stat_heap(0)[:slot_size] assert_raise(ArgumentError) { GC.stat_heap(-1) } - assert_raise(ArgumentError) { GC.stat_heap(GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT]) } + assert_raise(ArgumentError) { GC.stat_heap(GC::INTERNAL_CONSTANTS[:HEAP_COUNT]) } end def test_stat_heap_all stat_heap_all = {} stat_heap = {} + # Initialize to prevent GC in future calls + GC.stat_heap(0, stat_heap) + GC.stat_heap(nil, stat_heap_all) - 2.times do - GC.stat_heap(0, stat_heap) + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| GC.stat_heap(nil, stat_heap_all) - end - - GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT].times do |i| GC.stat_heap(i, stat_heap) + # Remove keys that can vary between invocations + %i(total_allocated_objects heap_live_slots heap_free_slots).each do |sym| + stat_heap[sym] = stat_heap_all[i][sym] = 0 + end + assert_equal stat_heap, stat_heap_all[i] end @@ -196,31 +279,51 @@ class TestGc < Test::Unit::TestCase stat = GC.stat stat_heap = GC.stat_heap - GC.stat(stat) - GC.stat_heap(nil, stat_heap) + 2.times do + GC.stat(stat) + GC.stat_heap(nil, stat_heap) + end stat_heap_sum = Hash.new(0) stat_heap.values.each do |hash| hash.each { |k, v| stat_heap_sum[k] += v } end - assert_equal stat[:heap_allocatable_pages], stat_heap_sum[:heap_allocatable_pages] + 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_tomb_pages], stat_heap_sum[:heap_tomb_pages] - assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] + stat_heap_sum[:heap_tomb_slots] - assert_equal stat[:total_allocated_pages], stat_heap_sum[:total_allocated_pages] - assert_equal stat[:total_freed_pages], stat_heap_sum[:total_freed_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] + assert_equal stat[:total_freed_objects], stat_heap_sum[:total_freed_objects] + end + + def test_measure_total_time + assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60) + GC.measure_total_time = false + + time_before = GC.stat(:time) + + # Generate some garbage + Random.new.bytes(100 * 1024 * 1024) + GC.start + + time_after = GC.stat(:time) + + # If time measurement is disabled, the time stat should not change + assert_equal time_before, time_after + RUBY end def test_latest_gc_info omit 'stress' if GC.stress - assert_separately %w[--disable-gem], __FILE__, __LINE__, <<-'eom' - GC.start - count = GC.stat(:heap_free_slots) + GC.stat(:heap_allocatable_pages) * GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] - count.times{ "a" + "b" } - assert_equal :newobj, GC.latest_gc_info[:gc_by] - eom + assert_separately([], __FILE__, __LINE__, <<-'RUBY') + GC.start + count = GC.stat(:heap_free_slots) + GC.stat(:heap_allocatable_slots) + count.times{ "a" + "b" } + assert_equal :newobj, GC.latest_gc_info[:gc_by] + RUBY GC.latest_gc_info(h = {}) # allocate hash and rehearsal GC.start @@ -256,20 +359,64 @@ class TestGc < Test::Unit::TestCase 3.times { GC.start } assert_nil GC.latest_gc_info(:need_major_by) - # allocate objects until need_major_by is set or major GC happens - major_count = GC.stat(:major_gc_count) - objects = [] - while GC.stat(:major_gc_count) == major_count && GC.latest_gc_info(:need_major_by).nil? - objects.append(100.times.map { '*' }) + EnvUtil.without_gc do + # allocate objects until need_major_by is set or major GC happens + objects = [] + while GC.latest_gc_info(:need_major_by).nil? + objects.append(100.times.map { '*' }) + GC.start(full_mark: false) + end + + # We need to ensure that no GC gets ran before the call to GC.start since + # it would trigger a major GC. Assertions could allocate objects and + # trigger a GC so we don't run assertions until we perform the major GC. + need_major_by = GC.latest_gc_info(:need_major_by) + GC.start(full_mark: false) # should be upgraded to major + major_by = GC.latest_gc_info(:major_by) + + assert_not_nil(need_major_by) + assert_not_nil(major_by) end + end - assert_not_nil GC.latest_gc_info(:need_major_by) - GC.start(full_mark: false) # should be upgraded to major - assert_not_nil GC.latest_gc_info(:major_by) + def test_latest_gc_info_weak_references_count + assert_separately([], __FILE__, __LINE__, <<~RUBY) + GC.disable + COUNT = 10_000 + # Some weak references may be created, so allow some margin of error + error_tolerance = 100 + + # Run full GC to collect stats about weak references + GC.start + + before_weak_references_count = GC.latest_gc_info(:weak_references_count) + + # 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) + + before_weak_references_count = GC.latest_gc_info(: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 GC all the WeakMaps + GC.start + + assert_operator(GC.latest_gc_info(:weak_references_count), :<=, before_weak_references_count - COUNT + error_tolerance) + RUBY end def test_stress_compile_send - assert_in_out_err(%w[--disable-gems], <<-EOS, [], [], "") + assert_in_out_err([], <<-EOS, [], [], "") GC.stress = true begin eval("A::B.c(1, 1, d: 234)") @@ -279,7 +426,7 @@ class TestGc < Test::Unit::TestCase end def test_singleton_method - assert_in_out_err(%w[--disable-gems], <<-EOS, [], [], "[ruby-dev:42832]") + assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:42832]") GC.stress = true 10.times do obj = Object.new @@ -291,7 +438,7 @@ class TestGc < Test::Unit::TestCase end def test_singleton_method_added - assert_in_out_err(%w[--disable-gems], <<-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) @@ -306,42 +453,19 @@ class TestGc < Test::Unit::TestCase end def test_gc_parameter - env = { - "RUBY_GC_HEAP_INIT_SLOTS" => "100" - } - assert_in_out_err([env, "-W0", "-e", "exit"], "", [], [], "[Bug #19284]") - - env = { - "RUBY_GC_MALLOC_LIMIT" => "60000000", - "RUBY_GC_HEAP_INIT_SLOTS" => "100000" - } - assert_normal_exit("exit", "[ruby-core:39777]", :child_env => env) - env = {} - GC.stat_heap.each do |_, s| - env["RUBY_GC_HEAP_INIT_SIZE_#{s[:slot_size]}_SLOTS"] = "200000" + GC.stat_heap.keys.each do |heap| + env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "200000" end assert_normal_exit("exit", "", :child_env => env) - env["RUBY_GC_HEAP_INIT_SLOTS"] = "100000" - assert_normal_exit("exit", "", :child_env => env) - env = {} - GC.stat_heap.each do |_, s| - env["RUBY_GC_HEAP_INIT_SIZE_#{s[:slot_size]}_SLOTS"] = "0" + GC.stat_heap.keys.each do |heap| + env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "0" end assert_normal_exit("exit", "", :child_env => env) env = { - "RUBYOPT" => "", - "RUBY_GC_HEAP_INIT_SLOTS" => "100000" - } - assert_in_out_err([env, "-e", "exit"], "", [], [], "[ruby-core:39795]") - assert_in_out_err([env, "-W0", "-e", "exit"], "", [], [], "[ruby-core:39795]") - assert_in_out_err([env, "-W1", "-e", "exit"], "", [], [], "[ruby-core:39795]") - assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_INIT_SLOTS=100000/, "[ruby-core:39795]") - - env = { "RUBY_GC_HEAP_GROWTH_FACTOR" => "2.0", "RUBY_GC_HEAP_GROWTH_MAX_SLOTS" => "10000" } @@ -349,17 +473,12 @@ class TestGc < Test::Unit::TestCase assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_FACTOR=2.0/, "") assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_MAX_SLOTS=10000/, "[ruby-core:57928]") - env = { - "RUBY_GC_HEAP_INIT_SLOTS" => "100000", - "RUBY_GC_HEAP_FREE_SLOTS" => "10000", - "RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0.4", - } - assert_normal_exit("exit", "", :child_env => env) - assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=0\.4/, "") - if use_rgengc? + env = { + "RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0.4", + } # always full GC when RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR < 1.0 - assert_in_out_err([env, "--disable-gems", "-e", "GC.start; 1000_000.times{Object.new}; p(GC.stat[:minor_gc_count] < GC.stat[:major_gc_count])"], "", ['true'], //, "") + assert_in_out_err([env, "-e", "GC.start; 1000_000.times{Object.new}; p(GC.stat[:minor_gc_count] < GC.stat[:major_gc_count])"], "", ['true'], //, "") end env = { @@ -383,6 +502,55 @@ class TestGc < Test::Unit::TestCase assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_MAX=16000000/, "") assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR=2.0/, "") end + + ["0.01", "0.1", "1.0"].each do |i| + env = {"RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0", "RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO" => i} + assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY) + GC.disable + GC.start + assert_equal((GC.stat[:old_objects] * #{i}).to_i, GC.stat[:remembered_wb_unprotected_objects_limit]) + RUBY + end + end + + def test_gc_parameter_init_slots + omit "[Bug #21203] This test is flaky and intermittently failing now" + + assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60) + # Constant from gc.c. + GC_HEAP_INIT_SLOTS = 10_000 + + gc_count = GC.stat(:count) + # Fill up all of the size pools to the init slots + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| + capa = (GC.stat_heap(i, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] + while GC.stat_heap(i, :heap_eden_slots) < GC_HEAP_INIT_SLOTS + Array.new(capa) + end + end + + assert_equal gc_count, GC.stat(:count) + RUBY + + env = {} + sizes = GC.stat_heap.keys.reverse.map { 20_000 } + GC.stat_heap.keys.each do |heap| + env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = sizes[heap].to_s + end + assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY, timeout: 60) + SIZES = #{sizes} + + gc_count = GC.stat(:count) + # Fill up all of the size pools to the init slots + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| + capa = (GC.stat_heap(i, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] + while GC.stat_heap(i, :heap_eden_slots) < SIZES[i] + Array.new(capa) + end + end + + assert_equal gc_count, GC.stat(:count) + RUBY end def test_profiler_enabled @@ -396,19 +564,27 @@ class TestGc < Test::Unit::TestCase def test_profiler_clear omit "for now" - assert_separately %w[--disable-gem], __FILE__, __LINE__, <<-'eom', timeout: 30 - GC::Profiler.enable + assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 30) + GC::Profiler.enable - GC.start - assert_equal(1, GC::Profiler.raw_data.size) - GC::Profiler.clear - assert_equal(0, GC::Profiler.raw_data.size) + GC.start + assert_equal(1, GC::Profiler.raw_data.size) + GC::Profiler.clear + assert_equal(0, GC::Profiler.raw_data.size) + + 200.times{ GC.start } + assert_equal(200, GC::Profiler.raw_data.size) + GC::Profiler.clear + assert_equal(0, GC::Profiler.raw_data.size) + RUBY + end - 200.times{ GC.start } - assert_equal(200, GC::Profiler.raw_data.size) - GC::Profiler.clear - assert_equal(0, GC::Profiler.raw_data.size) - eom + def test_profiler_raw_data + GC::Profiler.enable + GC.start + assert GC::Profiler.raw_data + ensure + GC::Profiler.disable end def test_profiler_total_time @@ -422,42 +598,53 @@ class TestGc < Test::Unit::TestCase end def test_finalizing_main_thread - assert_in_out_err(%w[--disable-gems], <<-EOS, ["\"finalize\""], [], "[ruby-dev:46647]") + assert_in_out_err([], <<-EOS, ["\"finalize\""], [], "[ruby-dev:46647]") ObjectSpace.define_finalizer(Thread.main) { p 'finalize' } EOS end def test_expand_heap - assert_separately %w[--disable-gem], __FILE__, __LINE__, <<-'eom' - GC.start - base_length = GC.stat[:heap_eden_pages] - (base_length * 500).times{ 'a' } - GC.start - base_length = GC.stat[:heap_eden_pages] - (base_length * 500).times{ 'a' } - GC.start - assert_in_epsilon base_length, (v = GC.stat[:heap_eden_pages]), 1/8r, - "invalid heap expanding (base_length: #{base_length}, GC.stat[:heap_eden_pages]: #{v})" + assert_separately([], __FILE__, __LINE__, <<~'RUBY') + GC.start + base_length = GC.stat[:heap_eden_pages] + (base_length * 500).times{ 'a' } + GC.start + base_length = GC.stat[:heap_eden_pages] + (base_length * 500).times{ 'a' } + GC.start + assert_in_epsilon base_length, (v = GC.stat[:heap_eden_pages]), 1/8r, + "invalid heap expanding (base_length: #{base_length}, GC.stat[:heap_eden_pages]: #{v})" - a = [] - (base_length * 500).times{ a << 'a'; nil } - GC.start - assert_operator base_length, :<, GC.stat[:heap_eden_pages] + 1 - eom + a = [] + (base_length * 500).times{ a << 'a'; nil } + GC.start + assert_operator base_length, :<, GC.stat[:heap_eden_pages] + 1 + RUBY end def test_thrashing_for_young_objects # This test prevents bugs like [Bug #18929] - assert_separately %w[--disable-gem], __FILE__, __LINE__, <<-'RUBY' + assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 60) # Grow the heap @ary = 100_000.times.map { Object.new } # Warmup to make sure heap stabilizes 1_000_000.times { Object.new } - before_stats = GC.stat + # We need to pre-allocate all the hashes for GC.stat calls, because + # otherwise the call to GC.stat/GC.stat_heap itself could cause a new + # page to be allocated and the before/after assertions will fail + before_stats = {} + after_stats = {} + # stat_heap needs a hash of hashes for each heap; easiest way to get the + # right shape for that is just to call stat_heap with no argument before_stat_heap = GC.stat_heap + after_stat_heap = GC.stat_heap + + # Now collect the actual stats + GC.stat before_stats + GC.stat_heap nil, before_stat_heap 1_000_000.times { Object.new } @@ -465,24 +652,50 @@ class TestGc < Test::Unit::TestCase # running a minor GC here will guarantee that GC will be complete GC.start(full_mark: false) - after_stats = GC.stat - after_stat_heap = GC.stat_heap + GC.stat after_stats + GC.stat_heap nil, after_stat_heap # Debugging output to for failures in trunk-repeat50@phosphorus-docker debug_msg = "before_stats: #{before_stats}\nbefore_stat_heap: #{before_stat_heap}\nafter_stats: #{after_stats}\nafter_stat_heap: #{after_stat_heap}" # Should not be thrashing in page creation assert_equal before_stats[:heap_allocated_pages], after_stats[:heap_allocated_pages], debug_msg - assert_equal 0, after_stats[:heap_tomb_pages], debug_msg assert_equal 0, after_stats[:total_freed_pages], debug_msg # Only young objects, so should not trigger major GC assert_equal before_stats[:major_gc_count], after_stats[:major_gc_count], debug_msg RUBY end + def test_heaps_grow_independently + # [Bug #21214] + + assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 60) + COUNT = 1_000_000 + + def allocate_small_object = [] + def allocate_large_object = Array.new(10) + + @arys = Array.new(COUNT) do + # Allocate 10 small transient objects + 10.times { allocate_small_object } + # Allocate 1 large object that is persistent + allocate_large_object + end + + # 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 + def test_gc_internals assert_not_nil GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] - assert_not_nil GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + assert_not_nil GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] end def test_sweep_in_finalizer @@ -513,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') @@ -528,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 @@ -539,11 +753,11 @@ class TestGc < Test::Unit::TestCase end def test_finalizer_passed_object_id - assert_in_out_err(%w[--disable-gems], <<-EOS, ["true"], []) + assert_in_out_err([], <<~RUBY, ["true"], []) o = Object.new obj_id = o.object_id ObjectSpace.define_finalizer(o, ->(id){ p id == obj_id }) - EOS + RUBY end def test_verify_internal_consistency @@ -573,13 +787,17 @@ class TestGc < Test::Unit::TestCase end def test_gc_disabled_start - begin - disabled = GC.disable + EnvUtil.without_gc do c = GC.count GC.start assert_equal 1, GC.count - c - ensure - GC.enable unless disabled + end + + EnvUtil.without_gc do + c = GC.count + GC.start(immediate_mark: false, immediate_sweep: false) + 10_000.times { Object.new } + assert_equal 1, GC.count - c end end @@ -591,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" @@ -611,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" @@ -628,6 +850,15 @@ class TestGc < Test::Unit::TestCase obj = nil end end; + + assert_normal_exit "#{<<~"begin;"}\n#{<<~'end;'}", '[Bug #20042]' + begin; + def (f = Object.new).call = nil # missing ID + o = Object.new + ObjectSpace.define_finalizer(o, f) + o = nil + GC.start + end; end def test_object_ids_never_repeat @@ -643,4 +874,49 @@ class TestGc < Test::Unit::TestCase Module.new.class_eval( (["# shareable_constant_value: literal"] + (0..100000).map {|i| "M#{ i } = {}" }).join("\n")) end + + def test_old_to_young_reference + EnvUtil.without_gc do + require "objspace" + + old_obj = Object.new + 4.times { GC.start } + + assert_include ObjectSpace.dump(old_obj), '"old":true' + + young_obj = Object.new + old_obj.instance_variable_set(:@test, young_obj) + + # Not immediately promoted to old generation + 3.times do + assert_not_include ObjectSpace.dump(young_obj), '"old":true' + GC.start + end + + # Takes 4 GC to promote to old generation + GC.start + 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 bef2ba9605..7e0c499dd9 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true require 'test/unit' -require 'fiddle' -require 'etc' if RUBY_PLATFORM =~ /s390x/ warn "Currently, it is known that the compaction does not work well on s390x; contribution is welcome https://github.com/ruby/ruby/pull/5077" @@ -10,8 +8,8 @@ end class TestGCCompact < Test::Unit::TestCase module CompactionSupportInspector - def supports_auto_compact? - GC::OPTS.include?("GC_COMPACTION_SUPPORTED") + def supports_compact? + GC.respond_to?(:compact) end end @@ -19,7 +17,7 @@ class TestGCCompact < Test::Unit::TestCase include CompactionSupportInspector def setup - omit "autocompact not supported on this platform" unless supports_auto_compact? + omit "GC compaction not supported on this platform" unless supports_compact? super end end @@ -32,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 @@ -85,7 +83,7 @@ class TestGCCompact < Test::Unit::TestCase include CompactionSupportInspector def assert_not_implemented(method, *args) - omit "autocompact is supported on this platform" if supports_auto_compact? + omit "autocompact is supported on this platform" if supports_compact? assert_raise(NotImplementedError) { GC.send(method, *args) } refute(GC.respond_to?(method), "GC.#{method} should be defined as rb_f_notimplement") @@ -112,10 +110,6 @@ class TestGCCompact < Test::Unit::TestCase end end - def os_page_size - return true unless defined?(Etc::SC_PAGE_SIZE) - end - def test_gc_compact_stats list = [] @@ -130,10 +124,6 @@ class TestGCCompact < Test::Unit::TestCase refute_predicate compact_stats[:moved], :empty? end - def memory_location(obj) - (Fiddle.dlwrap(obj) >> 1) - end - def big_list(level = 10) if level > 0 big_list(level - 1) @@ -146,21 +136,6 @@ class TestGCCompact < Test::Unit::TestCase end end - # Find an object that's allocated in a slot that had a previous - # tenant, and that tenant moved and is still alive - def find_object_in_recycled_slot(addresses) - new_object = nil - - 100_000.times do - new_object = Object.new - if addresses.index memory_location(new_object) - break - end - end - - new_object - end - def test_complex_hash_keys list_of_objects = big_list hash = list_of_objects.hash @@ -171,17 +146,17 @@ class TestGCCompact < Test::Unit::TestCase end def test_ast_compacts - assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; 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 @@ -210,7 +185,7 @@ class TestGCCompact < Test::Unit::TestCase end def test_updating_references_for_heap_allocated_shared_arrays - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; ary = [] 50.times { |i| ary << i } @@ -234,7 +209,7 @@ class TestGCCompact < Test::Unit::TestCase def test_updating_references_for_embed_shared_arrays omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; ary = Array.new(50) 50.times { |i| ary[i] = i } @@ -258,7 +233,7 @@ class TestGCCompact < Test::Unit::TestCase end def test_updating_references_for_heap_allocated_frozen_shared_arrays - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; ary = [] 50.times { |i| ary << i } @@ -283,7 +258,7 @@ class TestGCCompact < Test::Unit::TestCase def test_updating_references_for_embed_frozen_shared_arrays omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; ary = Array.new(50) 50.times { |i| ary[i] = i } @@ -308,52 +283,56 @@ class TestGCCompact < Test::Unit::TestCase end; end - def test_moving_arrays_down_size_pools + def test_moving_arrays_down_heaps omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; - ARY_COUNT = 500 + ARY_COUNT = 50000 GC.verify_compaction_references(expand_heap: true, toward: :empty) - arys = ARY_COUNT.times.map do - ary = "abbbbbbbbbb".chars - ary.uniq! - end + Fiber.new { + $arys = ARY_COUNT.times.map do + ary = "abbbbbbbbbb".chars + ary.uniq! + end + }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats.dig(:moved_down, :T_ARRAY) || 0, :>=, ARY_COUNT) - assert_include(ObjectSpace.dump(arys[0]), '"embedded":true') + assert_operator(stats.dig(:moved_down, :T_ARRAY) || 0, :>=, ARY_COUNT - 10) + refute_empty($arys.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end - def test_moving_arrays_up_size_pools + def test_moving_arrays_up_heaps omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; - ARY_COUNT = 500 + ARY_COUNT = 50000 GC.verify_compaction_references(expand_heap: true, toward: :empty) - ary = "hello".chars - arys = ARY_COUNT.times.map do - x = [] - ary.each { |e| x << e } - x - end + Fiber.new { + ary = "hello".chars + $arys = ARY_COUNT.times.map do + x = [] + ary.each { |e| x << e } + x + end + }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats.dig(:moved_up, :T_ARRAY) || 0, :>=, ARY_COUNT) - assert_include(ObjectSpace.dump(arys[0]), '"embedded":true') + assert_operator(stats.dig(:moved_up, :T_ARRAY) || 0, :>=, (0.9995 * ARY_COUNT).to_i) + refute_empty($arys.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end - def test_moving_objects_between_size_pools + def test_moving_objects_between_heaps omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 60) begin; class Foo def add_ivars @@ -363,61 +342,106 @@ class TestGCCompact < Test::Unit::TestCase end end - OBJ_COUNT = 500 + OBJ_COUNT = 50000 GC.verify_compaction_references(expand_heap: true, toward: :empty) - ary = OBJ_COUNT.times.map { Foo.new } - ary.each(&:add_ivars) + Fiber.new { + $ary = OBJ_COUNT.times.map { Foo.new } + $ary.each(&:add_ivars) - GC.start - Foo.new.add_ivars + GC.start + Foo.new.add_ivars + }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats.dig(:moved_up, :T_OBJECT) || 0, :>=, OBJ_COUNT) - assert_include(ObjectSpace.dump(ary[0]), '"embedded":true') + 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_moving_strings_up_size_pools + def test_compact_objects_of_varying_sizes omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; - STR_COUNT = 500 + $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 - str = "a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] - ary = STR_COUNT.times.map { "" << str } + def test_moving_strings_up_heaps + omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 30) + begin; + STR_COUNT = 50000 + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + Fiber.new { + str = "a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 4 + $ary = STR_COUNT.times.map { +"" << str } + }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats[:moved_up][:T_STRING], :>=, STR_COUNT) - assert_include(ObjectSpace.dump(ary[0]), '"embedded":true') + assert_operator(stats[:moved_up][:T_STRING], :>=, STR_COUNT - 15) + refute_empty($ary.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end - def test_moving_strings_down_size_pools + def test_moving_strings_down_heaps omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 - assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10, signal: :SEGV) + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 30) begin; - STR_COUNT = 500 + STR_COUNT = 50000 GC.verify_compaction_references(expand_heap: true, toward: :empty) - ary = STR_COUNT.times.map { ("a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE]).squeeze! } + Fiber.new { + $ary = STR_COUNT.times.map { ("a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 4).squeeze! } + }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) - assert_operator(stats[:moved_down][:T_STRING], :>=, STR_COUNT) - assert_include(ObjectSpace.dump(ary[0]), '"embedded":true') + assert_operator(stats[:moved_down][:T_STRING], :>=, STR_COUNT - 10) + refute_empty($ary.keep_if { |o| ObjectSpace.dump(o).include?('"embedded":true') }) end; end - def test_moving_objects_between_size_pools_keeps_shape_frozen_status + def test_moving_hashes_down_heaps + omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + # AR and ST hashes are in the same size pool on 32 bit + omit unless RbConfig::SIZEOF["uint64_t"] <= RbConfig::SIZEOF["void*"] + + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 30) + begin; + HASH_COUNT = 50000 + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + Fiber.new { + base_hash = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 } + $ary = HASH_COUNT.times.map { base_hash.dup } + $ary.each_with_index { |h, i| h[:i] = 9 } + }.resume + + stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_operator(stats[:moved_down][:T_HASH], :>=, HASH_COUNT - 10) + end; + end + + def test_moving_objects_between_heaps_keeps_shape_frozen_status # [Bug #19536] assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}") begin; @@ -444,4 +468,21 @@ class TestGCCompact < Test::Unit::TestCase assert_raise(FrozenError) { a.set_a } end; end + + def test_moving_too_complex_generic_ivar + omit "not compiled with SHAPE_DEBUG" unless defined?(RubyVM::Shape) + + assert_separately([], <<~RUBY) + RubyVM::Shape.exhaust_shapes + + obj = [] + obj.instance_variable_set(:@fixnum, 123) + obj.instance_variable_set(:@str, "hello") + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_equal(123, obj.instance_variable_get(:@fixnum)) + assert_equal("hello", obj.instance_variable_get(:@str)) + RUBY + end end diff --git a/test/ruby/test_hash.rb b/test/ruby/test_hash.rb index 83d16d462e..32384f5a5c 100644 --- a/test/ruby/test_hash.rb +++ b/test/ruby/test_hash.rb @@ -4,7 +4,6 @@ require 'test/unit' EnvUtil.suppress_warning {require 'continuation'} class TestHash < Test::Unit::TestCase - def test_hash x = @cls[1=>2, 2=>4, 3=>6] y = @cls[1=>2, 2=>4, 3=>6] # y = {1, 2, 2, 4, 3, 6} # 1.9 doesn't support @@ -85,20 +84,9 @@ class TestHash < Test::Unit::TestCase self => 'self', true => 'true', nil => 'nil', 'nil' => nil ] - @verbose = $VERBOSE end def teardown - $VERBOSE = @verbose - end - - def test_bad_initialize_copy - h = Class.new(Hash) { - def initialize_copy(h) - super(Object.new) - end - }.new - assert_raise(TypeError) { h.dup } end def test_clear_initialize_copy @@ -113,35 +101,6 @@ class TestHash < Test::Unit::TestCase assert_equal(2, h[1]) end - def test_dup_will_not_rehash - assert_hash_does_not_rehash(&:dup) - end - - def assert_hash_does_not_rehash - obj = Object.new - class << obj - attr_accessor :hash_calls - def hash - @hash_calls += 1 - super - end - end - obj.hash_calls = 0 - hash = {obj => 42} - assert_equal(1, obj.hash_calls) - yield hash - assert_equal(1, obj.hash_calls) - end - - def test_select_reject_will_not_rehash - assert_hash_does_not_rehash do |hash| - hash.select { true } - end - assert_hash_does_not_rehash do |hash| - hash.reject { false } - end - end - def test_s_AREF_from_hash h = @cls["a" => 100, "b" => 200] assert_equal(100, h['a']) @@ -219,6 +178,16 @@ class TestHash < Test::Unit::TestCase assert_equal('default', h['spurious']) end + def test_st_literal_memory_leak + assert_no_memory_leak([], "", "#{<<~"begin;"}\n#{<<~'end;'}", rss: true) + begin; + 1_000_000.times do + # >8 element hashes are ST allocated rather than AR allocated + {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9} + end + end; + end + def test_try_convert assert_equal({1=>2}, Hash.try_convert({1=>2})) assert_equal(nil, Hash.try_convert("1=>2")) @@ -294,78 +263,6 @@ class TestHash < Test::Unit::TestCase assert_equal(256, h[z]) end - def test_AREF_fstring_key - # warmup ObjectSpace.count_objects - ObjectSpace.count_objects - - h = {"abc" => 1} - before = ObjectSpace.count_objects[:T_STRING] - 5.times{ h["abc"] } - assert_equal before, ObjectSpace.count_objects[:T_STRING] - end - - def test_AREF_fstring_key_default_proc - assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") - begin; - h = Hash.new do |h, k| - k.frozen? - end - - str = "foo" - refute str.frozen? # assumes this file is frozen_string_literal: false - refute h[str] - refute h["foo"] - end; - end - - def test_ASET_fstring_key - a, b = {}, {} - assert_equal 1, a["abc"] = 1 - assert_equal 1, b["abc"] = 1 - assert_same a.keys[0], b.keys[0] - end - - def test_ASET_fstring_non_literal_key - underscore = "_" - non_literal_strings = Proc.new{ ["abc#{underscore}def", "abc" * 5, "abc" + "def", "" << "ghi" << "jkl"] } - - a, b = {}, {} - non_literal_strings.call.each do |string| - assert_equal 1, a[string] = 1 - end - - non_literal_strings.call.each do |string| - assert_equal 1, b[string] = 1 - end - - [a.keys, b.keys].transpose.each do |key_a, key_b| - assert_same key_a, key_b - end - end - - def test_hash_aset_fstring_identity - h = {}.compare_by_identity - h['abc'] = 1 - h['abc'] = 2 - assert_equal 2, h.size, '[ruby-core:78783] [Bug #12855]' - end - - def test_hash_aref_fstring_identity - h = {}.compare_by_identity - h['abc'] = 1 - assert_nil h['abc'], '[ruby-core:78783] [Bug #12855]' - end - - def test_NEWHASH_fstring_key - a = {"ABC" => :t} - b = {"ABC" => :t} - assert_same a.keys[0], b.keys[0] - assert_same "ABC".freeze, a.keys[0] - var = +'ABC' - c = { var => :t } - assert_same "ABC".freeze, c.keys[0] - end - def test_EQUAL # '==' h1 = @cls[ "a" => 1, "c" => 2 ] h2 = @cls[ "a" => 1, "c" => 2, 7 => 35 ] @@ -472,6 +369,10 @@ class TestHash < Test::Unit::TestCase end end assert_equal(base.dup, h) + + h = base.dup + assert_same h, h.delete_if {h.assoc(nil); true} + assert_empty h end def test_keep_if @@ -842,14 +743,6 @@ class TestHash < Test::Unit::TestCase assert_predicate(h, :compare_by_identity?) end - def test_replace_bug15358 - h1 = {} - h2 = {a:1,b:2,c:3,d:4,e:5} - h2.replace(h1) - GC.start - assert(true) - end - def test_shift h = @h.dup @@ -965,13 +858,6 @@ class TestHash < Test::Unit::TestCase assert_instance_of(Hash, h) end - def test_nil_to_h - h = nil.to_h - assert_equal({}, h) - assert_nil(h.default) - assert_nil(h.default_proc) - end - def test_to_s h = @cls[ 1 => 2, "cat" => "dog", 1.5 => :fred ] assert_equal(h.inspect, h.to_s) @@ -983,6 +869,34 @@ class TestHash < Test::Unit::TestCase $, = nil end + def test_inspect + no_quote = '{a: 1, a!: 1, a?: 1}' + quote0 = '{"": 1}' + quote1 = '{"0": 1, "!": 1, "%": 1, "&": 1, "*": 1, "+": 1, "-": 1, "/": 1, "<": 1, ">": 1, "^": 1, "`": 1, "|": 1, "~": 1}' + quote2 = '{"@a": 1, "$a": 1, "+@": 1, "a=": 1, "[]": 1}' + quote3 = '{"a\"b": 1, "@@a": 1, "<=>": 1, "===": 1, "[]=": 1}' + assert_equal(no_quote, eval(no_quote).inspect) + assert_equal(quote0, eval(quote0).inspect) + assert_equal(quote1, eval(quote1).inspect) + assert_equal(quote2, eval(quote2).inspect) + assert_equal(quote3, eval(quote3).inspect) + + EnvUtil.with_default_external(Encoding::ASCII) do + utf8_ascii_hash = '{"\\u3042": 1}' + assert_equal(eval(utf8_ascii_hash).inspect, utf8_ascii_hash) + end + + EnvUtil.with_default_external(Encoding::UTF_8) do + utf8_hash = "{\u3042: 1}" + assert_equal(eval(utf8_hash).inspect, utf8_hash) + end + + EnvUtil.with_default_external(Encoding::Windows_31J) do + sjis_hash = "{\x87]: 1}".force_encoding('sjis') + assert_equal(eval(sjis_hash).inspect, sjis_hash) + end + end + def test_update h1 = @cls[ 1 => 2, 2 => 3, 3 => 4 ] h2 = @cls[ 2 => 'two', 4 => 'four' ] @@ -1018,12 +932,6 @@ class TestHash < Test::Unit::TestCase assert_equal([], expected - vals) end - def test_initialize_wrong_arguments - assert_raise(ArgumentError) do - Hash.new(0) { } - end - end - def test_create assert_equal({1=>2, 3=>4}, @cls[[[1,2],[3,4]]]) assert_raise(ArgumentError) { @cls[0, 1, 2] } @@ -1305,15 +1213,6 @@ class TestHash < Test::Unit::TestCase assert_raise(FrozenError) { h2.replace(42) } end - def test_replace_memory_leak - assert_no_memory_leak([], "#{<<-"begin;"}", "#{<<-'end;'}") - h = ("aa".."zz").each_with_index.to_h - 10_000.times {h.dup} - begin; - 500_000.times {h.dup.replace(h)} - end; - end - def test_size2 assert_equal(0, @cls[].size) end @@ -1397,6 +1296,26 @@ 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 + i[key] = 0 + h = @cls[].update(i) + key.upcase! + assert_equal(0, h.fetch('a')) + end + def test_merge h1 = @cls[1=>2, 3=>4] h2 = {1=>3, 5=>7} @@ -1419,10 +1338,10 @@ class TestHash < Test::Unit::TestCase expected[7] = 8 h2 = h.merge(7=>8) assert_equal(expected, h2) - assert_equal(true, h2.compare_by_identity?) + assert_predicate(h2, :compare_by_identity?) h2 = h.merge({}) assert_equal(h, h2) - assert_equal(true, h2.compare_by_identity?) + assert_predicate(h2, :compare_by_identity?) h = @cls[] h.compare_by_identity @@ -1430,10 +1349,10 @@ class TestHash < Test::Unit::TestCase h1.compare_by_identity h2 = h.merge(7=>8) assert_equal(h1, h2) - assert_equal(true, h2.compare_by_identity?) + assert_predicate(h2, :compare_by_identity?) h2 = h.merge({}) assert_equal(h, h2) - assert_equal(true, h2.compare_by_identity?) + assert_predicate(h2, :compare_by_identity?) end def test_merge! @@ -1488,6 +1407,8 @@ class TestHash < Test::Unit::TestCase end def test_callcc + omit 'requires callcc support' unless respond_to?(:callcc) + h = @cls[1=>2] c = nil f = false @@ -1508,6 +1429,8 @@ class TestHash < Test::Unit::TestCase end def test_callcc_iter_level + omit 'requires callcc support' unless respond_to?(:callcc) + bug9105 = '[ruby-dev:47803] [Bug #9105]' h = @cls[1=>2, 3=>4] c = nil @@ -1526,6 +1449,8 @@ class TestHash < Test::Unit::TestCase end def test_callcc_escape + omit 'requires callcc support' unless respond_to?(:callcc) + bug9105 = '[ruby-dev:47803] [Bug #9105]' assert_nothing_raised(RuntimeError, bug9105) do h=@cls[] @@ -1540,6 +1465,8 @@ class TestHash < Test::Unit::TestCase end def test_callcc_reenter + omit 'requires callcc support' unless respond_to?(:callcc) + bug9105 = '[ruby-dev:47803] [Bug #9105]' assert_nothing_raised(RuntimeError, bug9105) do h = @cls[1=>2,3=>4] @@ -1587,6 +1514,16 @@ class TestHash < Test::Unit::TestCase assert_predicate(h.dup, :compare_by_identity?, bug8703) end + def test_compare_by_identy_memory_leak + assert_no_memory_leak([], "", "#{<<~"begin;"}\n#{<<~'end;'}", "[Bug #20145]", rss: true) + begin; + h = { 1 => 2 }.compare_by_identity + 1_000_000.times do + h.select { false } + end + end; + end + def test_same_key bug9646 = '[ruby-dev:48047] [Bug #9646] Infinite loop at Hash#each' h = @cls[a=[], 1] @@ -1717,115 +1654,6 @@ class TestHash < Test::Unit::TestCase end end - def test_exception_in_rehash_memory_leak - return unless @cls == Hash - - bug9187 = '[ruby-core:58728] [Bug #9187]' - - prepare = <<-EOS - class Foo - def initialize - @raise = false - end - - def hash - raise if @raise - @raise = true - return 0 - end - end - h = {Foo.new => true} - EOS - - code = <<-EOS - 10_0000.times do - h.rehash rescue nil - end - GC.start - EOS - - assert_no_memory_leak([], prepare, code, bug9187) - end - - def test_wrapper - bug9381 = '[ruby-core:59638] [Bug #9381]' - - wrapper = Class.new do - def initialize(obj) - @obj = obj - end - - def hash - @obj.hash - end - - def eql?(other) - @obj.eql?(other) - end - end - - bad = [ - 5, true, false, nil, - 0.0, 1.72723e-77, - :foo, "dsym_#{self.object_id.to_s(16)}_#{Time.now.to_i.to_s(16)}".to_sym, - "str", - ].select do |x| - hash = {x => bug9381} - hash[wrapper.new(x)] != bug9381 - end - assert_empty(bad, bug9381) - end - - def assert_hash_random(obj, dump = obj.inspect) - a = [obj.hash.to_s] - 3.times { - assert_in_out_err(["-e", "print (#{dump}).hash"], "") do |r, e| - a += r - assert_equal([], e) - end - } - assert_not_equal([obj.hash.to_s], a.uniq) - assert_operator(a.uniq.size, :>, 2, proc {a.inspect}) - end - - def test_string_hash_random - assert_hash_random('abc') - end - - def test_symbol_hash_random - assert_hash_random(:-) - assert_hash_random(:foo) - assert_hash_random("dsym_#{self.object_id.to_s(16)}_#{Time.now.to_i.to_s(16)}".to_sym) - end - - def test_integer_hash_random - assert_hash_random(0) - assert_hash_random(+1) - assert_hash_random(-1) - assert_hash_random(+(1<<100)) - assert_hash_random(-(1<<100)) - end - - def test_float_hash_random - assert_hash_random(0.0) - assert_hash_random(+1.0) - assert_hash_random(-1.0) - assert_hash_random(1.72723e-77) - assert_hash_random(Float::INFINITY, "Float::INFINITY") - end - - def test_label_syntax - return unless @cls == Hash - - feature4935 = '[ruby-core:37553] [Feature #4935]' - x = 'world' - hash = assert_nothing_raised(SyntaxError, feature4935) do - break eval(%q({foo: 1, "foo-bar": 2, "hello-#{x}": 3, 'hello-#{x}': 4, 'bar': {}})) - end - assert_equal({:foo => 1, :'foo-bar' => 2, :'hello-world' => 3, :'hello-#{x}' => 4, :bar => {}}, hash, feature4935) - x = x - end - def test_dig h = @cls[a: @cls[b: [1, 2, 3]], c: 4] assert_equal(1, h.dig(:a, :b, 0)) @@ -1845,12 +1673,12 @@ class TestHash < Test::Unit::TestCase def o.respond_to?(*args) super end - assert_raise(TypeError, bug12030) {{foo: o}.dig(:foo, :foo)} + assert_raise(TypeError, bug12030) {@cls[foo: o].dig(:foo, :foo)} end def test_cmp - h1 = {a:1, b:2} - h2 = {a:1, b:2, c:3} + h1 = @cls[a:1, b:2] + h2 = @cls[a:1, b:2, c:3] assert_operator(h1, :<=, h1) assert_operator(h1, :<=, h2) @@ -1874,8 +1702,8 @@ class TestHash < Test::Unit::TestCase end def test_cmp_samekeys - h1 = {a:1} - h2 = {a:2} + h1 = @cls[a:1] + h2 = @cls[a:2] assert_operator(h1, :<=, h1) assert_not_operator(h1, :<=, h2) @@ -1899,15 +1727,15 @@ class TestHash < Test::Unit::TestCase end def test_to_proc - h = { + h = @cls[ 1 => 10, 2 => 20, 3 => 30, - } + ] assert_equal([10, 20, 30], [1, 2, 3].map(&h)) - assert_equal(true, h.to_proc.lambda?) + assert_predicate(h.to_proc, :lambda?) end def test_transform_keys @@ -2035,22 +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) - end - def test_broken_hash_value - bug14218 = '[ruby-core:84395] [Bug #14218]' - - assert_equal(0, 1_000_000.times.count{a=Object.new.hash; b=Object.new.hash; a < 0 && b < 0 && a + b > 0}, bug14218) - assert_equal(0, 1_000_000.times.count{a=Object.new.hash; b=Object.new.hash; 0 + a + b != 0 + b + a}, bug14218) - end - - def test_reserved_hash_val - s = Struct.new(:hash) - h = {} - keys = [*0..8] - keys.each {|i| h[s.new(i)]=true} - msg = proc {h.inspect} - assert_equal(keys, h.keys.map(&:hash), msg) + 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 @@ -2082,6 +1902,27 @@ class TestHash < Test::Unit::TestCase # ignore end + # Previously this test would fail because rb_hash inside opt_aref would look + # at the current method name + def test_hash_recursion_independent_of_mid + o = Class.new do + def hash(h, k) + h[k] + end + + def any_other_name(h, k) + h[k] + end + end.new + + rec = []; rec << rec + + h = @cls[] + h[rec] = 1 + assert o.hash(h, rec) + assert o.any_other_name(h, rec) + end + class TestSubHash < TestHash class SubHash < Hash end @@ -2091,6 +1932,346 @@ class TestHash < Test::Unit::TestCase super end end +end + +class TestHashOnly < Test::Unit::TestCase + def test_bad_initialize_copy + h = Class.new(Hash) { + def initialize_copy(h) + super(Object.new) + end + }.new + assert_raise(TypeError) { h.dup } + end + + def test_dup_will_not_rehash + assert_hash_does_not_rehash(&:dup) + end + + def assert_hash_does_not_rehash + obj = Object.new + class << obj + attr_accessor :hash_calls + def hash + @hash_calls += 1 + super + end + end + obj.hash_calls = 0 + hash = {obj => 42} + assert_equal(1, obj.hash_calls) + yield hash + assert_equal(1, obj.hash_calls) + end + + def test_select_reject_will_not_rehash + assert_hash_does_not_rehash do |hash| + hash.select { true } + end + assert_hash_does_not_rehash do |hash| + hash.reject { false } + end + end + + def test_st_literal_memory_leak + assert_no_memory_leak([], "", "#{<<~"begin;"}\n#{<<~'end;'}", rss: true) + begin; + 1_000_000.times do + # >8 element hashes are ST allocated rather than AR allocated + {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9} + end + end; + end + + def test_compare_by_id_memory_leak + assert_no_memory_leak([], "", <<~RUBY, rss: true) + 1_000_000.times do + {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8}.compare_by_identity + end + RUBY + end + + def test_try_convert + assert_equal({1=>2}, Hash.try_convert({1=>2})) + assert_equal(nil, Hash.try_convert("1=>2")) + o = Object.new + def o.to_hash; {3=>4} end + assert_equal({3=>4}, Hash.try_convert(o)) + end + + def test_AREF_fstring_key + # warmup ObjectSpace.count_objects + ObjectSpace.count_objects + + h = {"abc" => 1} + + EnvUtil.without_gc do + before = ObjectSpace.count_objects[:T_STRING] + 5.times{ h["abc".freeze] } + assert_equal before, ObjectSpace.count_objects[:T_STRING] + end + end + + def test_AREF_fstring_key_default_proc + assert_separately(['--disable-frozen-string-literal'], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + h = Hash.new do |h, k| + k.frozen? + end + + str = "foo" + refute str.frozen? + refute h[str] + refute h["foo"] + end; + end + + def test_ASET_fstring_key + a, b = {}, {} + assert_equal 1, a["abc"] = 1 + assert_equal 1, b["abc"] = 1 + assert_same a.keys[0], b.keys[0] + end + + def test_ASET_fstring_non_literal_key + underscore = "_" + non_literal_strings = Proc.new{ ["abc#{underscore}def", "abc" * 5, "abc" + "def", "" << "ghi" << "jkl"] } + + a, b = {}, {} + non_literal_strings.call.each do |string| + assert_equal 1, a[string] = 1 + end + + non_literal_strings.call.each do |string| + assert_equal 1, b[string] = 1 + end + + [a.keys, b.keys].transpose.each do |key_a, key_b| + assert_same key_a, key_b + end + end + + def test_hash_aset_fstring_identity + h = {}.compare_by_identity + h['abc'] = 1 + h['abc'] = 2 + assert_equal 2, h.size, '[ruby-core:78783] [Bug #12855]' + end + + def test_hash_aref_fstring_identity + h = {}.compare_by_identity + h['abc'] = 1 + assert_nil h['abc'], '[ruby-core:78783] [Bug #12855]' + end + + def test_NEWHASH_fstring_key + a = {"ABC" => :t} + b = {"ABC" => :t} + assert_same a.keys[0], b.keys[0] + assert_same "ABC".freeze, a.keys[0] + var = +'ABC' + c = { var => :t } + assert_same "ABC".freeze, c.keys[0] + end + + def test_rehash_memory_leak + assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true) + ar_hash = 1.times.map { |i| [i, i] }.to_h + st_hash = 10.times.map { |i| [i, i] }.to_h + + code = proc do + ar_hash.rehash + st_hash.rehash + end + 1_000.times(&code) + PREP + 1_000_000.times(&code) + CODE + end + + def test_replace_bug15358 + h1 = {} + h2 = {a:1,b:2,c:3,d:4,e:5} + h2.replace(h1) + GC.start + assert(true) + end + + def test_replace_st_with_ar + # ST hash + h1 = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9 } + # AR hash + h2 = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7 } + # Replace ST hash with AR hash + h1.replace(h2) + assert_equal(h2, h1) + end + + def test_nil_to_h + h = nil.to_h + assert_equal({}, h) + assert_nil(h.default) + assert_nil(h.default_proc) + end + + def test_initialize_wrong_arguments + assert_raise(ArgumentError) do + Hash.new(0) { } + end + end + + def test_replace_memory_leak + assert_no_memory_leak([], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true) + h = ("aa".."zz").each_with_index.to_h + 10_000.times {h.dup} + begin; + 500_000.times {h.dup.replace(h)} + end; + end + + def hash_iter_recursion(h, level) + return if level == 0 + h.each_key {} + h.each_value { hash_iter_recursion(h, level - 1) } + end + + def test_iterlevel_in_ivar_bug19589 + h = { a: nil } + # 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 + + def test_exception_in_rehash_memory_leak + bug9187 = '[ruby-core:58728] [Bug #9187]' + + prepare = <<-EOS + class Foo + def initialize + @raise = false + end + + def hash + raise if @raise + @raise = true + return 0 + end + end + h = {Foo.new => true} + EOS + + code = <<-EOS + 10_0000.times do + h.rehash rescue nil + end + GC.start + EOS + + assert_no_memory_leak([], prepare, code, bug9187) + end + + def test_memory_size_after_delete + require 'objspace' + h = {} + 1000.times {|i| h[i] = true} + big = ObjectSpace.memsize_of(h) + 1000.times {|i| h.delete(i)} + assert_operator ObjectSpace.memsize_of(h), :<, big/10 + end + + def test_wrapper + bug9381 = '[ruby-core:59638] [Bug #9381]' + + wrapper = Class.new do + def initialize(obj) + @obj = obj + end + + def hash + @obj.hash + end + + def eql?(other) + @obj.eql?(other) + end + end + + bad = [ + 5, true, false, nil, + 0.0, 1.72723e-77, + :foo, "dsym_#{self.object_id.to_s(16)}_#{Time.now.to_i.to_s(16)}".to_sym, + "str", + ].select do |x| + hash = {x => bug9381} + hash[wrapper.new(x)] != bug9381 + end + assert_empty(bad, bug9381) + end + + def assert_hash_random(obj, dump = obj.inspect) + a = [obj.hash.to_s] + 3.times { + assert_in_out_err(["-e", "print (#{dump}).hash"], "") do |r, e| + a += r + assert_equal([], e) + end + } + assert_not_equal([obj.hash.to_s], a.uniq) + assert_operator(a.uniq.size, :>, 2, proc {a.inspect}) + end + + def test_string_hash_random + assert_hash_random('abc') + end + + def test_symbol_hash_random + assert_hash_random(:-) + assert_hash_random(:foo) + assert_hash_random("dsym_#{self.object_id.to_s(16)}_#{Time.now.to_i.to_s(16)}".to_sym) + end + + def test_integer_hash_random + assert_hash_random(0) + assert_hash_random(+1) + assert_hash_random(-1) + assert_hash_random(+(1<<100)) + assert_hash_random(-(1<<100)) + end + + def test_float_hash_random + assert_hash_random(0.0) + assert_hash_random(+1.0) + assert_hash_random(-1.0) + assert_hash_random(1.72723e-77) + assert_hash_random(Float::INFINITY, "Float::INFINITY") + end + + def test_label_syntax + feature4935 = '[ruby-core:37553] [Feature #4935]' + x = 'world' + hash = assert_nothing_raised(SyntaxError, feature4935) do + break eval(%q({foo: 1, "foo-bar": 2, "hello-#{x}": 3, 'hello-#{x}': 4, 'bar': {}})) + end + assert_equal({:foo => 1, :'foo-bar' => 2, :'hello-world' => 3, :'hello-#{x}' => 4, :bar => {}}, hash, feature4935) + x = x + end + + def test_broken_hash_value + bug14218 = '[ruby-core:84395] [Bug #14218]' + + assert_equal(0, 1_000_000.times.count{a=Object.new.hash; b=Object.new.hash; a < 0 && b < 0 && a + b > 0}, bug14218) + assert_equal(0, 1_000_000.times.count{a=Object.new.hash; b=Object.new.hash; 0 + a + b != 0 + b + a}, bug14218) + end + + def test_reserved_hash_val + s = Struct.new(:hash) + h = {} + keys = [*0..8] + keys.each {|i| h[s.new(i)]=true} + msg = proc {h.inspect} + assert_equal(keys, h.keys.map(&:hash), msg) + end ruby2_keywords def get_flagged_hash(*args) args.last @@ -2116,23 +2297,11 @@ class TestHash < Test::Unit::TestCase assert_raise(TypeError) { Hash.ruby2_keywords_hash(1) } end - def test_ar2st - # insert - obj = Object.new - obj.instance_variable_set(:@h, h = {}) - def obj.hash - 10.times{|i| @h[i] = i} - 0 - end - def obj.inspect - 'test' + def ar2st_object + class << (obj = Object.new) + attr_reader :h end - h[obj] = true - assert_equal '{0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5, 6=>6, 7=>7, 8=>8, 9=>9, test=>true}', h.inspect - - # delete - obj = Object.new - obj.instance_variable_set(:@h, h = {}) + obj.instance_variable_set(:@h, {}) def obj.hash 10.times{|i| @h[i] = i} 0 @@ -2143,6 +2312,21 @@ class TestHash < Test::Unit::TestCase def obj.eql? other other.class == Object end + obj + end + + def test_ar2st_insert + obj = ar2st_object + h = obj.h + + h[obj] = true + assert_equal '{0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9, test => true}', h.inspect + end + + def test_ar2st_delete + obj = ar2st_object + h = obj.h + obj2 = Object.new def obj2.hash 0 @@ -2150,21 +2334,13 @@ class TestHash < Test::Unit::TestCase h[obj2] = true h.delete obj - assert_equal '{0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5, 6=>6, 7=>7, 8=>8, 9=>9}', h.inspect + assert_equal '{0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9}', h.inspect + end + + def test_ar2st_lookup + obj = ar2st_object + h = obj.h - # lookup - obj = Object.new - obj.instance_variable_set(:@h, h = {}) - def obj.hash - 10.times{|i| @h[i] = i} - 0 - end - def obj.inspect - 'test' - end - def obj.eql? other - other.class == Object - end obj2 = Object.new def obj2.hash 0 @@ -2180,25 +2356,9 @@ class TestHash < Test::Unit::TestCase end end - # Previously this test would fail because rb_hash inside opt_aref would look - # at the current method name - def test_hash_recursion_independent_of_mid - o = Class.new do - def hash(h, k) - h[k] - end - - def any_other_name(h, k) - h[k] - end - end.new - - rec = []; rec << rec - - h = @cls[] - h[rec] = 1 - assert o.hash(h, rec) - assert o.any_other_name(h, rec) + def test_bug_21357 + h = {x: []}.merge(x: nil) { |_k, v1, _v2| v1 } + assert_equal({x: []}, h) end def test_any_hash_fixable @@ -2226,4 +2386,49 @@ class TestHash < Test::Unit::TestCase end; end end + + def test_compare_by_identity_during_iteration + h = { 1 => 1 } + h.each do + assert_raise(RuntimeError, "compare_by_identity during iteration") do + h.compare_by_identity + end + end + end + + def test_ar_hash_to_st_hash + assert_normal_exit("#{<<~"begin;"}\n#{<<~'end;'}", 'https://bugs.ruby-lang.org/issues/20050#note-5') + begin; + srand(0) + class Foo + def to_a + [] + end + + def hash + $h.delete($h.keys.sample) if rand < 0.1 + to_a.hash + end + end + + 1000.times do + $h = {} + (0..10).each {|i| $h[Foo.new] ||= {} } + end + end; + end + + def test_ar_to_st_reserved_value + klass = Class.new do + attr_reader :hash + def initialize(val) = @hash = val + end + + values = 0.downto(-16).to_a + hash = {} + values.each do |val| + hash[klass.new(val)] = val + end + assert_equal values, hash.values, "[ruby-core:121239] [Bug #21170]" + end end diff --git a/test/ruby/test_integer.rb b/test/ruby/test_integer.rb index a3e64ddb38..f9bf4fa20c 100644 --- a/test/ruby/test_integer.rb +++ b/test/ruby/test_integer.rb @@ -57,20 +57,19 @@ class TestInteger < Test::Unit::TestCase nil end, "[ruby-dev:32084] [ruby-dev:34547]") - x = EnvUtil.suppress_warning {2 ** -0x4000000000000000} - assert_in_delta(0.0, (x / 2), Float::EPSILON) + assert_raise(ArgumentError) {2 ** -0x4000000000000000} <<~EXPRS.each_line.with_index(__LINE__+1) do |expr, line| crash01: 111r+11**-11111161111111 crash02: 1118111111111**-1111111111111111**1+1==11111 - crash03: -1111111**-1111*11 - -1111111** -111111111 + crash03: -1111111**-1111*11 - -11** -1111111 crash04: 1118111111111** -1111111111111111**1+11111111111**1 ===111 crash05: 11** -111155555555555555 -55 !=5-555 crash07: 1 + 111111111**-1111811111 crash08: 18111111111**-1111111111111111**1 + 1111111111**-1111**1 crash10: -7 - -1111111** -1111**11 crash12: 1118111111111** -1111111111111111**1 + 1111 - -1111111** -1111*111111111119 - crash13: 1.0i - -1111111** -111111111 + crash13: 1.0i - -11** -1111111 crash14: 11111**111111111**111111 * -11111111111111111111**-111111111111 crash15: ~1**1111 + -~1**~1**111 crash17: 11** -1111111**1111 /11i @@ -80,7 +79,7 @@ class TestInteger < Test::Unit::TestCase crash21: 11**-10111111119-1i -1r EXPRS name, expr = expr.split(':', 2) - assert_ruby_status(%w"-W0", expr, name) + assert_ruby_status(%w"-W0", "begin; #{ expr }; rescue ArgumentError; end", name) end end @@ -138,20 +137,6 @@ class TestInteger < Test::Unit::TestCase assert_equal(1234, Integer(1234)) assert_equal(1, Integer(1.234)) - # base argument - assert_equal(1234, Integer("1234", 10)) - assert_equal(668, Integer("1234", 8)) - assert_equal(4660, Integer("1234", 16)) - assert_equal(49360, Integer("1234", 36)) - # decimal, not octal - assert_equal(1234, Integer("01234", 10)) - assert_raise(ArgumentError) { Integer("0x123", 10) } - assert_raise(ArgumentError) { Integer(1234, 10) } - assert_raise(ArgumentError) { Integer(12.34, 10) } - assert_raise(ArgumentError) { Integer(Object.new, 1) } - - assert_raise(ArgumentError) { Integer(1, 1, 1) } - assert_equal(2 ** 50, Integer(2.0 ** 50)) assert_raise(TypeError) { Integer(nil) } @@ -173,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 @@ -252,6 +239,32 @@ class TestInteger < Test::Unit::TestCase assert_equal(16, Integer(obj)) end + def test_Integer_with_base + assert_equal(1234, Integer("1234", 10)) + assert_equal(668, Integer("1234", 8)) + assert_equal(4660, Integer("1234", 16)) + assert_equal(49360, Integer("1234", 36)) + # decimal, not octal + assert_equal(1234, Integer("01234", 10)) + assert_raise(ArgumentError) { Integer("0x123", 10) } + assert_raise(ArgumentError) { Integer(1234, 10) } + assert_raise(ArgumentError) { Integer(12.34, 10) } + assert_raise(ArgumentError) { Integer(Object.new, 1) } + + assert_raise(ArgumentError) { Integer(1, 1, 1) } + + def (base = Object.new).to_int + 8 + end + assert_equal(8, Integer("10", base)) + + assert_raise(TypeError) { Integer("10", "8") } + def (base = Object.new).to_int + "8" + end + assert_raise(TypeError) { Integer("10", base) } + end + def test_int_p assert_not_predicate(1.0, :integer?) assert_predicate(1, :integer?) @@ -270,31 +283,31 @@ class TestInteger < Test::Unit::TestCase def test_upto a = [] - 1.upto(3) {|x| a << x } + assert_equal(1, 1.upto(3) {|x| a << x }) assert_equal([1, 2, 3], a) a = [] - 1.upto(0) {|x| a << x } + assert_equal(1, 1.upto(0) {|x| a << x }) assert_equal([], a) y = 2**30 - 1 a = [] - y.upto(y+2) {|x| a << x } + assert_equal(y, y.upto(y+2) {|x| a << x }) assert_equal([y, y+1, y+2], a) end def test_downto a = [] - -1.downto(-3) {|x| a << x } + assert_equal(-1, -1.downto(-3) {|x| a << x }) assert_equal([-1, -2, -3], a) a = [] - 1.downto(2) {|x| a << x } + assert_equal(1, 1.downto(2) {|x| a << x }) assert_equal([], a) y = -(2**30) a = [] - y.downto(y-2) {|x| a << x } + assert_equal(y, y.downto(y-2) {|x| a << x }) assert_equal([y, y-1, y-2], a) end @@ -309,23 +322,34 @@ class TestInteger < Test::Unit::TestCase begin; called = false Integer.class_eval do - alias old_plus + - undef + - define_method(:+){|x| called = true; 1} + alias old_succ succ + undef succ + define_method(:succ){|x| called = true; x+1} alias old_lt < undef < define_method(:<){|x| called = true} end + + fix = 1 + fix.times{break 0} + fix_called = called + + called = false + big = 2**65 big.times{break 0} + big_called = called + Integer.class_eval do - undef + - alias + old_plus + undef succ + alias succ old_succ undef < alias < old_lt end + + # Asssert that Fixnum and Bignum behave consistently bug18377 = "[ruby-core:106361]" - assert_equal(false, called, bug18377) + assert_equal(fix_called, big_called, bug18377) end; end @@ -442,6 +466,10 @@ class TestInteger < Test::Unit::TestCase assert_int_equal(1111_1111_1111_1111_1111_1111_1111_1111, 1111_1111_1111_1111_1111_1111_1111_1111.floor(1)) assert_int_equal(10**400, (10**400).floor(1)) + + assert_int_equal(-10000000000, -1.floor(-10), "[Bug #20654]") + assert_int_equal(-100000000000000000000, -1.floor(-20), "[Bug #20654]") + assert_int_equal(-100000000000000000000000000000000000000000000000000, -1.floor(-50), "[Bug #20654]") end def test_ceil @@ -470,6 +498,10 @@ class TestInteger < Test::Unit::TestCase assert_int_equal(1111_1111_1111_1111_1111_1111_1111_1111, 1111_1111_1111_1111_1111_1111_1111_1111.ceil(1)) assert_int_equal(10**400, (10**400).ceil(1)) + + assert_int_equal(10000000000, 1.ceil(-10), "[Bug #20654]") + assert_int_equal(100000000000000000000, 1.ceil(-20), "[Bug #20654]") + assert_int_equal(100000000000000000000000000000000000000000000000000, 1.ceil(-50), "[Bug #20654]") end def test_truncate @@ -678,9 +710,21 @@ class TestInteger < Test::Unit::TestCase assert_equal(x, Integer.sqrt(x ** 2), "[ruby-core:95453]") end + def test_bug_21217 + assert_equal(0x10000 * 2**10, Integer.sqrt(0x100000008 * 2**20)) + end + def test_fdiv assert_equal(1.0, 1.fdiv(1)) assert_equal(0.5, 1.fdiv(2)) + + m = 50 << Float::MANT_DIG + prev = 1.0 + (1..100).each do |i| + val = (m + i).fdiv(m) + assert_operator val, :>=, prev, "1+epsilon*(#{i}/100)" + prev = val + end end def test_obj_fdiv diff --git a/test/ruby/test_io.rb b/test/ruby/test_io.rb index 0716dd7bf9..1adf47ac51 100644 --- a/test/ruby/test_io.rb +++ b/test/ruby/test_io.rb @@ -350,6 +350,19 @@ class TestIO < Test::Unit::TestCase end) end + def test_ungetc_with_seek + make_tempfile {|t| + t.open + t.write('0123456789') + t.rewind + + t.ungetc('a') + t.seek(2, :SET) + + assert_equal('2', t.getc) + } + end + def test_ungetbyte make_tempfile {|t| t.open @@ -373,6 +386,19 @@ class TestIO < Test::Unit::TestCase } end + def test_ungetbyte_with_seek + make_tempfile {|t| + t.open + t.write('0123456789') + t.rewind + + t.ungetbyte('a'.ord) + t.seek(2, :SET) + + assert_equal('2'.ord, t.getbyte) + } + end + def test_each_byte pipe(proc do |w| w << "abc def" @@ -441,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 @@ -655,7 +699,6 @@ class TestIO < Test::Unit::TestCase if have_nonblock? def test_copy_stream_no_busy_wait - omit "RJIT has busy wait on GC. This sometimes fails with --jit." if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? omit "multiple threads already active" if Thread.list.size > 1 msg = 'r58534 [ruby-core:80969] [Backport #13533]' @@ -1116,6 +1159,34 @@ class TestIO < Test::Unit::TestCase } end + def test_copy_stream_dup_buffer + bug21131 = '[ruby-core:120961] [Bug #21131]' + mkcdtmpdir do + dst_class = Class.new do + def initialize(&block) + @block = block + end + + def write(data) + @block.call(data.dup) + data.bytesize + end + end + + rng = Random.new(42) + body = Tempfile.new("ruby-bug", binmode: true) + body.write(rng.bytes(16_385)) + body.rewind + + payload = [] + IO.copy_stream(body, dst_class.new{payload << it}) + body.rewind + assert_equal(body.read, payload.join, bug21131) + ensure + body&.close + end + end + def test_copy_stream_write_in_binmode bug8767 = '[ruby-core:56518] [Bug #8767]' mkcdtmpdir { @@ -1679,7 +1750,6 @@ class TestIO < Test::Unit::TestCase end if have_nonblock? def test_read_nonblock_no_exceptions - omit '[ruby-core:90895] RJIT worker may leave fd open in a forked child' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # TODO: consider acquiring GVL from RJIT worker. with_pipe {|r, w| assert_equal :wait_readable, r.read_nonblock(4096, exception: false) w.puts "HI!" @@ -1898,6 +1968,148 @@ class TestIO < Test::Unit::TestCase end) end + def test_readline_bad_param_raises + File.open(__FILE__) do |f| + assert_raise(TypeError) do + f.readline Object.new + end + end + + File.open(__FILE__) do |f| + assert_raise(TypeError) do + f.readline 1, 2 + end + end + end + + def test_readline_raises + File.open(__FILE__) do |f| + assert_equal File.read(__FILE__), f.readline(nil) + assert_raise(EOFError) do + f.readline + end + end + end + + def test_readline_separators + File.open(__FILE__) do |f| + line = f.readline("def") + assert_equal File.read(__FILE__)[/\A.*?def/m], line + end + + File.open(__FILE__) do |f| + line = f.readline("def", chomp: true) + assert_equal File.read(__FILE__)[/\A.*?(?=def)/m], line + end + end + + def test_readline_separators_limits + t = Tempfile.open("readline_limit") + str = "#" * 50 + sep = "def" + + t.write str + t.write sep + t.write str + t.flush + + # over limit + File.open(t.path) do |f| + line = f.readline sep, str.bytesize + assert_equal(str, line) + end + + # under limit + File.open(t.path) do |f| + line = f.readline(sep, str.bytesize + 5) + assert_equal(str + sep, line) + end + + # under limit + chomp + File.open(t.path) do |f| + line = f.readline(sep, str.bytesize + 5, chomp: true) + assert_equal(str, line) + end + ensure + t&.close! + end + + def test_readline_limit_without_separator + t = Tempfile.open("readline_limit") + str = "#" * 50 + sep = "\n" + + t.write str + t.write sep + t.write str + t.flush + + # over limit + File.open(t.path) do |f| + line = f.readline str.bytesize + assert_equal(str, line) + end + + # under limit + File.open(t.path) do |f| + line = f.readline(str.bytesize + 5) + assert_equal(str + sep, line) + end + + # under limit + chomp + File.open(t.path) do |f| + line = f.readline(str.bytesize + 5, chomp: true) + assert_equal(str, line) + end + ensure + t&.close! + end + + def test_readline_chomp_true + File.open(__FILE__) do |f| + line = f.readline(chomp: true) + assert_equal File.readlines(__FILE__).first.chomp, line + end + end + + def test_readline_incompatible_rs + first_line = File.open(__FILE__, &:gets).encode("utf-32le") + File.open(__FILE__, encoding: "utf-8:utf-32le") {|f| + assert_equal first_line, f.readline + assert_raise(ArgumentError) {f.readline("\0")} + } + end + + def test_readline_limit_nonascii + mkcdtmpdir do + i = 0 + + File.open("text#{i+=1}", "w+:utf-8") do |f| + f.write("Test\nok\u{bf}ok\n") + f.rewind + + assert_equal("Test\nok\u{bf}", f.readline("\u{bf}")) + assert_equal("ok\n", f.readline("\u{bf}")) + end + + File.open("text#{i+=1}", "w+b:utf-32le") do |f| + f.write("0123456789") + f.rewind + + assert_equal(4, f.readline(4).bytesize) + assert_equal(4, f.readline(3).bytesize) + end + + File.open("text#{i+=1}", "w+:utf-8:utf-32le") do |f| + f.write("0123456789") + f.rewind + + assert_equal(4, f.readline(4).bytesize) + assert_equal(4, f.readline(3).bytesize) + end + end + end + def test_set_lineno_readline pipe(proc do |w| w.puts "foo" @@ -2347,10 +2559,6 @@ class TestIO < Test::Unit::TestCase end def test_autoclose_true_closed_by_finalizer - # http://ci.rvm.jp/results/trunk-rjit@silicon-docker/1465760 - # http://ci.rvm.jp/results/trunk-rjit@silicon-docker/1469765 - omit 'this randomly fails with RJIT' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? - feature2250 = '[ruby-core:26222]' pre = 'ft2250' t = Tempfile.new(pre) @@ -2411,30 +2619,15 @@ class TestIO < Test::Unit::TestCase assert_equal({:a=>1}, open(o, {a: 1})) end - def test_open_pipe - open("|" + EnvUtil.rubybin, "r+") do |f| - f.puts "puts 'foo'" - f.close_write - assert_equal("foo\n", f.read) - end - end + def test_path_with_pipe + mkcdtmpdir do + cmd = "|echo foo" + assert_file.not_exist?(cmd) - def test_read_command - assert_equal("foo\n", IO.read("|echo foo")) - 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 - IO.read("|echo foo", 1, 1) + 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 @@ -2537,6 +2730,17 @@ class TestIO < Test::Unit::TestCase } end + def test_reopen_binmode + f1 = File.open(__FILE__) + f2 = File.open(__FILE__) + f1.binmode + f1.reopen(f2) + assert_not_operator(f1, :binmode?) + ensure + f2.close + f1.close + end + def make_tempfile_for_encoding t = make_tempfile open(t.path, "rb+:utf-8") {|f| f.puts "\u7d05\u7389bar\n"} @@ -2567,6 +2771,16 @@ class TestIO < Test::Unit::TestCase } end + def test_reopen_encoding_from_io + f1 = File.open(__FILE__, "rb:UTF-16LE") + f2 = File.open(__FILE__, "r:UTF-8") + f1.reopen(f2) + assert_equal(Encoding::UTF_8, f1.external_encoding) + ensure + f2.close + f1.close + end + def test_reopen_opt_encoding feature7103 = '[ruby-core:47806]' make_tempfile_for_encoding {|t| @@ -2618,14 +2832,6 @@ class TestIO < Test::Unit::TestCase end def test_foreach - a = [] - IO.foreach("|" + EnvUtil.rubybin + " -e 'puts :foo; puts :bar; puts :baz'") {|x| a << x } - assert_equal(["foo\n", "bar\n", "baz\n"], a) - - a = [] - IO.foreach("|" + EnvUtil.rubybin + " -e 'puts :zot'", :open_args => ["r"]) {|x| a << x } - assert_equal(["zot\n"], a) - make_tempfile {|t| a = [] IO.foreach(t.path) {|x| a << x } @@ -2701,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')} @@ -2814,6 +3020,15 @@ class TestIO < Test::Unit::TestCase f.close assert_equal("FOO\n", File.read(t.path)) + + fd = IO.sysopen(t.path) + %w[w r+ w+ a+].each do |mode| + assert_raise(Errno::EINVAL, "#{mode} [ruby-dev:38571]") {IO.new(fd, mode)} + end + f = IO.new(fd, "r") + data = f.read + f.close + assert_equal("FOO\n", data) } end @@ -2866,6 +3081,9 @@ class TestIO < Test::Unit::TestCase assert_equal("foo\nbar\nbaz\n", File.read(t.path)) assert_equal("foo\nba", File.read(t.path, 6)) assert_equal("bar\n", File.read(t.path, 4, 4)) + + assert_raise(ArgumentError) { File.read(t.path, -1) } + assert_raise(ArgumentError) { File.read(t.path, 1, -1) } } end @@ -3592,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") } @@ -3749,8 +3967,10 @@ __END__ end def test_open_fifo_does_not_block_other_threads - mkcdtmpdir { + mkcdtmpdir do File.mkfifo("fifo") + rescue NotImplementedError + else assert_separately([], <<-'EOS') t1 = Thread.new { open("fifo", "r") {|r| @@ -3765,8 +3985,32 @@ __END__ t1_value, _ = assert_join_threads([t1, t2]) assert_equal("foo", t1_value) EOS - } - end if /mswin|mingw|bccwin|cygwin/ !~ RUBY_PLATFORM + end + end + + def test_open_fifo_restart_at_signal_intterupt + mkcdtmpdir do + File.mkfifo("fifo") + rescue NotImplementedError + else + wait = EnvUtil.apply_timeout_scale(0.1) + data = "writing to fifo" + + # Do not use assert_separately, because reading from stdin + # prevents to reproduce [Bug #20708] + assert_in_out_err(["-e", "#{<<~"begin;"}\n#{<<~'end;'}"], [], [data]) + wait, data = #{wait}, #{data.dump} + ; + begin; + trap(:USR1) {} + Thread.new do + sleep wait; Process.kill(:USR1, $$) + sleep wait; File.write("fifo", data) + end + puts File.read("fifo") + end; + end + end if Signal.list[:USR1] # Pointless on platforms without signal def test_open_flag make_tempfile do |t| @@ -3811,7 +4055,7 @@ __END__ end def test_race_gets_and_close - opt = { signal: :ABRT, timeout: 200 } + opt = { signal: :ABRT, timeout: 10 } assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}", **opt) bug13076 = '[ruby-core:78845] [Bug #13076]' begin; @@ -3973,7 +4217,7 @@ __END__ assert_raise(EOFError) { f.pread(1, f.size) } end } - end if IO.method_defined?(:pread) + end def test_pwrite make_tempfile { |t| @@ -3982,7 +4226,7 @@ __END__ assert_equal("ooo", f.pread(3, 4)) end } - end if IO.method_defined?(:pread) and IO.method_defined?(:pwrite) + end def test_select_exceptfds if Etc.uname[:sysname] == 'SunOS' @@ -4002,6 +4246,23 @@ __END__ end end if Socket.const_defined?(:MSG_OOB) + def test_select_timeout + assert_equal(nil, IO.select(nil,nil,nil,0)) + assert_equal(nil, IO.select(nil,nil,nil,0.0)) + assert_raise(TypeError) { IO.select(nil,nil,nil,"invalid-timeout") } + assert_raise(ArgumentError) { IO.select(nil,nil,nil,-1) } + assert_raise(ArgumentError) { IO.select(nil,nil,nil,-0.1) } + assert_raise(ArgumentError) { IO.select(nil,nil,nil,-Float::INFINITY) } + assert_raise(RangeError) { IO.select(nil,nil,nil,Float::NAN) } + IO.pipe {|r, w| + w << "x" + ret = [[r], [], []] + assert_equal(ret, IO.select([r],nil,nil,0.1)) + assert_equal(ret, IO.select([r],nil,nil,1)) + assert_equal(ret, IO.select([r],nil,nil,Float::INFINITY)) + } + end + def test_recycled_fd_close dot = -'.' IO.pipe do |sig_rd, sig_wr| @@ -4113,4 +4374,55 @@ __END__ end end end + + def test_blocking_timeout + assert_separately([], <<~'RUBY') + IO.pipe do |r, w| + trap(:INT) do + w.puts "INT" + end + + main = Thread.current + thread = Thread.new do + # Wait until the main thread has entered `$stdin.gets`: + Thread.pass until main.status == 'sleep' + + # Cause an interrupt while handling `$stdin.gets`: + Process.kill :INT, $$ + end + + r.timeout = 1 + assert_equal("INT", r.gets.chomp) + rescue IO::TimeoutError + # Ignore - some platforms don't support interrupting `gets`. + ensure + thread&.join + 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 55e347471d..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,14 +74,66 @@ 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 NoMethodError do + assert_raise TypeError do IO::Buffer.map("foobar") end end @@ -88,24 +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? - - # Cannot modify string as it's locked by the buffer: - assert_raise RuntimeError do - string[0] = "h" - end + refute_predicate buffer, :readonly? buffer.set_value(:U8, 0, "h".ord) @@ -116,6 +164,26 @@ class TestIOBuffer < Test::Unit::TestCase end end + def test_string_mapped_buffer_locked + string = "Hello World" + IO::Buffer.for(string) do |buffer| + # Cannot modify string as it's locked by the buffer: + assert_raise RuntimeError do + string[0] = "h" + end + 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 @@ -156,6 +224,24 @@ class TestIOBuffer < Test::Unit::TestCase assert_equal message, buffer.get_string(0, message.bytesize) end + def test_resize_zero_internal + buffer = IO::Buffer.new(1) + + buffer.resize(0) + assert_equal 0, buffer.size + + buffer.resize(1) + assert_equal 1, buffer.size + end + + def test_resize_zero_external + buffer = IO::Buffer.for('1') + + assert_raise IO::Buffer::AccessError do + buffer.resize(0) + end + end + def test_compare_same_size buffer1 = IO::Buffer.new(1) assert_equal buffer1, buffer1 @@ -176,6 +262,14 @@ class TestIOBuffer < Test::Unit::TestCase assert_positive buffer2 <=> buffer1 end + def test_compare_zero_length + buffer1 = IO::Buffer.new(0) + buffer2 = IO::Buffer.new(1) + + assert_negative buffer1 <=> buffer2 + assert_positive buffer2 <=> buffer1 + end + def test_slice buffer = IO::Buffer.new(128) slice = buffer.slice(8, 32) @@ -205,6 +299,43 @@ class TestIOBuffer < Test::Unit::TestCase end end + def test_slice_readonly + hello = %w"Hello World".join(" ").freeze + buffer = IO::Buffer.for(hello) + slice = buffer.slice + assert_predicate slice, :readonly? + assert_raise IO::Buffer::AccessError do + # This breaks the literal in string pool and many other tests in this file. + slice.set_string("Adios", 0, 5) + end + assert_equal "Hello World", hello + end + + def test_transfer + hello = %w"Hello World".join(" ") + buffer = IO::Buffer.for(hello) + transferred = buffer.transfer + assert_equal "Hello World", transferred.get_string + assert_predicate buffer, :null? + assert_raise IO::Buffer::AccessError do + transferred.set_string("Goodbye") + end + assert_equal "Hello World", hello + end + + def test_transfer_in_block + hello = %w"Hello World".join(" ") + buffer = IO::Buffer.for(hello, &:transfer) + assert_equal "Hello World", buffer.get_string + buffer.set_string("Ciao!") + assert_equal "Ciao! World", hello + hello.freeze + assert_raise IO::Buffer::AccessError do + buffer.set_string("Hola") + end + assert_equal "Ciao! World", hello + end + def test_locked buffer = IO::Buffer.new(128, IO::Buffer::INTERNAL|IO::Buffer::LOCKED) @@ -233,6 +364,26 @@ class TestIOBuffer < Test::Unit::TestCase chunk = buffer.get_string(0, message.bytesize, Encoding::BINARY) assert_equal Encoding::BINARY, chunk.encoding + + assert_raise_with_message(ArgumentError, /bigger than the buffer size/) do + buffer.get_string(0, 129) + end + + assert_raise_with_message(ArgumentError, /bigger than the buffer size/) do + buffer.get_string(129) + end + + assert_raise_with_message(ArgumentError, /Offset can't be negative/) do + buffer.get_string(-1) + end + end + + def test_zero_length_get_string + buffer = IO::Buffer.new.slice(0, 0) + assert_equal "", buffer.get_string + + buffer = IO::Buffer.new(0) + assert_equal "", buffer.get_string end # We check that values are correctly round tripped. @@ -255,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) @@ -267,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 @@ -281,6 +449,13 @@ class TestIOBuffer < Test::Unit::TestCase end end + def test_zero_length_get_set_values + buffer = IO::Buffer.new(0) + + assert_equal [], buffer.get_values([], 0) + assert_equal 0, buffer.set_values([], 0, []) + end + def test_values buffer = IO::Buffer.new(128) @@ -305,16 +480,43 @@ class TestIOBuffer < Test::Unit::TestCase end end + def test_zero_length_each + buffer = IO::Buffer.new(0) + + assert_equal [], buffer.each(:U8).to_a + end + def test_each_byte string = "The quick brown fox jumped over the lazy dog." 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 + buffer = IO::Buffer.new(0) + + assert_equal [], buffer.each_byte.to_a end 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 @@ -343,21 +545,49 @@ class TestIOBuffer < Test::Unit::TestCase input.close end - def test_read - # This is currently a bug in IO:Buffer [#19084] which affects extended - # strings. On 32 bit machines, the example below becomes extended, so - # we omit this test until the bug is fixed. - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + def hello_world_tempfile(repeats = 1) io = Tempfile.new - io.write("Hello World") + repeats.times do + io.write("Hello World") + end io.seek(0) - buffer = IO::Buffer.new(128) - buffer.read(io, 5) - - assert_equal "Hello", buffer.get_string(0, 5) + yield io ensure - io.close! if io + io&.close! + end + + def test_read + hello_world_tempfile do |io| + buffer = IO::Buffer.new(128) + buffer.read(io) + assert_equal "Hello", buffer.get_string(0, 5) + end + end + + def test_read_with_with_length + hello_world_tempfile do |io| + buffer = IO::Buffer.new(128) + buffer.read(io, 5) + assert_equal "Hello", buffer.get_string(0, 5) + end + end + + def test_read_with_with_offset + hello_world_tempfile do |io| + buffer = IO::Buffer.new(128) + buffer.read(io, nil, 6) + assert_equal "Hello", buffer.get_string(6, 5) + end + end + + def test_read_with_length_and_offset + hello_world_tempfile(100) do |io| + buffer = IO::Buffer.new(1024) + # Only read 24 bytes from the file, as we are starting at offset 1000 in the buffer. + assert_equal 24, buffer.read(io, 0, 1000) + assert_equal "Hello World", buffer.get_string(1000, 11) + end end def test_write @@ -365,7 +595,7 @@ class TestIOBuffer < Test::Unit::TestCase buffer = IO::Buffer.new(128) buffer.set_string("Hello") - buffer.write(io, 5) + buffer.write(io) io.seek(0) assert_equal "Hello", io.read(5) @@ -373,6 +603,19 @@ class TestIOBuffer < Test::Unit::TestCase io.close! end + def test_write_with_length_and_offset + io = Tempfile.new + + buffer = IO::Buffer.new(5) + buffer.set_string("Hello") + buffer.write(io, 4, 1) + + io.seek(0) + assert_equal "ello", io.read(4) + ensure + io.close! + end + def test_pread io = Tempfile.new io.write("Hello World") @@ -465,4 +708,226 @@ class TestIOBuffer < Test::Unit::TestCase rescue NotImplementedError omit "Fork/shared memory is not supported." end + + def test_private + Tempfile.create(%w"buffer .txt") do |file| + file.write("Hello World") + + buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + begin + assert_predicate buffer, :private? + refute_predicate buffer, :readonly? + + buffer.set_string("J") + + # It was not changed because the mapping was private: + file.seek(0) + assert_equal "Hello World", file.read + ensure + buffer&.free + end + end + end + + def test_copy_overlapped_fwd + buf = IO::Buffer.for('0123456789').dup + buf.copy(buf, 3, 7) + assert_equal '0120123456', buf.get_string + end + + def test_copy_overlapped_bwd + buf = IO::Buffer.for('0123456789').dup + buf.copy(buf, 0, 7, 3) + assert_equal '3456789789', buf.get_string + end + + def test_copy_null_destination + buf = IO::Buffer.new(0) + assert_predicate buf, :null? + buf.copy(IO::Buffer.for('a'), 0, 0) + assert_predicate buf, :empty? + end + + def test_copy_null_source + buf = IO::Buffer.for('a').dup + src = IO::Buffer.new(0) + assert_predicate src, :null? + buf.copy(src, 0, 0) + assert_equal 'a', buf.get_string + end + + def test_set_string_overlapped_fwd + str = +'0123456789' + IO::Buffer.for(str) do |buf| + buf.set_string(str, 3, 7) + end + assert_equal '0120123456', str + end + + def test_set_string_overlapped_bwd + str = +'0123456789' + IO::Buffer.for(str) do |buf| + buf.set_string(str, 0, 7, 3) + end + assert_equal '3456789789', str + end + + def test_set_string_null_destination + buf = IO::Buffer.new(0) + assert_predicate buf, :null? + 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 267975d4ac..83d4fb0c7b 100644 --- a/test/ruby/test_io_m17n.rb +++ b/test/ruby/test_io_m17n.rb @@ -1395,26 +1395,6 @@ EOT } end - def test_open_pipe_r_enc - 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 - - def test_open_pipe_r_enc2 - 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 - def test_s_foreach_enc with_tmpdir { generate_file("t", "\xff") @@ -2744,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| @@ -2826,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 3692a697d6..fa716787fe 100644 --- a/test/ruby/test_iseq.rb +++ b/test/ruby/test_iseq.rb @@ -92,7 +92,17 @@ class TestISeq < Test::Unit::TestCase 42 end EOF - assert_equal(42, ISeq.load_from_binary(iseq.to_binary).eval) + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) + end + + def test_forwardable + iseq = compile(<<~EOF, __LINE__+1) + Class.new { + def bar(a, b); a + b; end + def foo(...); bar(...); end + } + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval.new.foo(40, 2)) end def test_super_with_block @@ -102,7 +112,7 @@ class TestISeq < Test::Unit::TestCase end 42 EOF - assert_equal(42, ISeq.load_from_binary(iseq.to_binary).eval) + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) end def test_super_with_block_hash_0 @@ -113,7 +123,7 @@ class TestISeq < Test::Unit::TestCase end 42 EOF - assert_equal(42, ISeq.load_from_binary(iseq.to_binary).eval) + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) end def test_super_with_block_and_kwrest @@ -123,17 +133,16 @@ class TestISeq < Test::Unit::TestCase end 42 EOF - assert_equal(42, ISeq.load_from_binary(iseq.to_binary).eval) + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) end 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).eval) + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) end def test_super_with_anonymous_block @@ -143,32 +152,36 @@ class TestISeq < Test::Unit::TestCase end 42 EOF - assert_equal(42, ISeq.load_from_binary(iseq.to_binary).eval) + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) end 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 + def test_ractor_shareable_value_frozen_core + iseq = RubyVM::InstructionSequence.compile(<<~'RUBY') + # shareable_constant_value: literal + REGEX = /#{}/ # [Bug #20569] + RUBY + assert_includes iseq_to_binary(iseq), "REGEX".b + end + def test_disasm_encoding - src = "\u{3042} = 1; \u{3042}; \u{3043}" + src = +"\u{3042} = 1; \u{3042}; \u{3043}" asm = compile(src).disasm assert_equal(src.encoding, asm.encoding) assert_predicate(asm, :valid_encoding?) @@ -279,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 @@ -347,11 +410,17 @@ class TestISeq < Test::Unit::TestCase end end assert_equal([m1, e1.message], [m2, e2.message], feature11951) - message = e1.message.each_line - message.with_index(1) do |line, i| - next if /^ / =~ line - assert_send([line, :start_with?, __FILE__], - proc {message.map {|l, j| (i == j ? ">" : " ") + l}.join("")}) + + if e1.message.lines[0] == "#{__FILE__}:#{line}: syntax errors found\n" + # Prism lays out the error messages in line with the source, so the + # following assertions do not make sense in that context. + else + message = e1.message.each_line + message.with_index(1) do |line, i| + next if /^ / =~ line + assert_send([line, :start_with?, __FILE__], + proc {message.map {|l, j| (i == j ? ">" : " ") + l}.join("")}) + end end end @@ -367,7 +436,7 @@ class TestISeq < Test::Unit::TestCase f.puts "end" f.close path = f.path - assert_in_out_err(%W[- #{path}], "#{<<-"begin;"}\n#{<<-"end;"}", /unexpected `end'/, [], success: true) + assert_in_out_err(%W[- #{path}], "#{<<-"begin;"}\n#{<<-"end;"}", /unexpected 'end'/, [], success: true) begin; path = ARGV[0] begin @@ -463,7 +532,7 @@ class TestISeq < Test::Unit::TestCase ["<class:C>@1", ["bar@10", ["block in bar@11", ["block (2 levels) in bar@12"]]], - ["foo@2", ["ensure in foo@2"], + ["foo@2", ["ensure in foo@7"], ["rescue in foo@4"]]], ["<class:D>@17"]] @@ -496,8 +565,9 @@ class TestISeq < Test::Unit::TestCase [4, :line], [7, :line], [9, :return]]], - [["ensure in foo@2", [[7, :line]]]], - [["rescue in foo@4", [[5, :line]]]]]], + [["ensure in foo@7", [[7, :line]]]], + [["rescue in foo@4", [[5, :line], + [5, :rescue]]]]]], [["<class:D>@17", [[17, :class], [18, :end]]]]], collect_iseq.call(sample_iseq) end @@ -541,16 +611,20 @@ class TestISeq < Test::Unit::TestCase } end + def iseq_to_binary(iseq) + iseq.to_binary + rescue RuntimeError => e + omit e.message if /compile with coverage/ =~ e.message + raise + end + def assert_iseq_to_binary(code, mesg = nil) iseq = RubyVM::InstructionSequence.compile(code) bin = assert_nothing_raised(mesg) do - iseq.to_binary - rescue RuntimeError => e - omit e.message if /compile with coverage/ =~ e.message - raise + iseq_to_binary(iseq) end 10.times do - bin2 = iseq.to_binary + bin2 = iseq_to_binary(iseq) assert_equal(bin, bin2, message(mesg) {diff hexdump(bin), hexdump(bin2)}) end iseq2 = RubyVM::InstructionSequence.load_from_binary(bin) @@ -565,6 +639,23 @@ class TestISeq < Test::Unit::TestCase iseq2 end + def test_to_binary_with_hidden_local_variables + assert_iseq_to_binary("for _foo in bar; end") + + bin = iseq_to_binary(RubyVM::InstructionSequence.compile(<<-RUBY)) + Object.new.instance_eval do + a = [] + def self.bar; [1] end + for foo in bar + a << (foo * 2) + end + a + end + RUBY + v = RubyVM::InstructionSequence.load_from_binary(bin).eval + assert_equal([2], v) + end + def test_to_binary_with_objects assert_iseq_to_binary("[]"+100.times.map{|i|"<</#{i}/"}.join) assert_iseq_to_binary("@x ||= (1..2)") @@ -626,7 +717,7 @@ class TestISeq < Test::Unit::TestCase end RUBY - iseq_bin = iseq.to_binary + iseq_bin = iseq_to_binary(iseq) iseq = ISeq.load_from_binary(iseq_bin) lines = [] TracePoint.new(tracepoint_type){|tp| @@ -684,18 +775,24 @@ class TestISeq < Test::Unit::TestCase end def test_iseq_of - [proc{}, - method(:test_iseq_of), - RubyVM::InstructionSequence.compile("p 1", __FILE__)].each{|src| + [ + proc{}, + method(:test_iseq_of), + RubyVM::InstructionSequence.compile("p 1", __FILE__), + begin; raise "error"; rescue => error; error.backtrace_locations[0]; end + ].each{|src| iseq = RubyVM::InstructionSequence.of(src) assert_equal __FILE__, iseq.path } end def test_iseq_of_twice_for_same_code - [proc{}, - method(:test_iseq_of_twice_for_same_code), - RubyVM::InstructionSequence.compile("p 1")].each{|src| + [ + proc{}, + method(:test_iseq_of_twice_for_same_code), + RubyVM::InstructionSequence.compile("p 1"), + begin; raise "error"; rescue => error; error.backtrace_locations[0]; end + ].each{|src| iseq1 = RubyVM::InstructionSequence.of(src) iseq2 = RubyVM::InstructionSequence.of(src) @@ -716,7 +813,7 @@ class TestISeq < Test::Unit::TestCase def test_iseq_builtin_load Tempfile.create(["builtin", ".iseq"]) do |f| f.binmode - f.write(RubyVM::InstructionSequence.of(1.method(:abs)).to_binary) + f.write(iseq_to_binary(RubyVM::InstructionSequence.of(1.method(:abs)))) f.close assert_separately(["-", f.path], "#{<<~"begin;"}\n#{<<~'end;'}") begin; @@ -756,7 +853,144 @@ class TestISeq < Test::Unit::TestCase GC.start Float(30) } - assert_equal :new, r.take + assert_equal :new, r.value + RUBY + end + + def test_ever_condition_loop + assert_ruby_status([], "BEGIN {exit}; while true && true; end") + end + + def test_unreachable_syntax_error + mesg = /Invalid break/ + assert_syntax_error("false and break", mesg) + assert_syntax_error("if false and break; end", mesg) + end + + def test_unreachable_pattern_matching + assert_in_out_err([], "true or 1 in 1") + assert_in_out_err([], "true or (case 1; in 1; 1; in 2; 2; end)") + end + + def test_unreachable_pattern_matching_in_if_condition + assert_in_out_err([], "#{<<~"begin;"}\n#{<<~'end;'}", %w[1]) + begin; + if true or {a: 0} in {a:} + p 1 + else + p a + end + end; + end + + def test_unreachable_next_in_block + bug20344 = '[ruby-core:117210] [Bug #20344]' + assert_nothing_raised(SyntaxError, bug20344) do + compile(<<~RUBY) + proc do + next + + case nil + when "a" + next + when "b" + when "c" + proc {} + end + + next + end + RUBY + 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 = RubyVM::InstructionSequence.compile("foo(bar: :baz)").to_binary + begin; + 1_000_000.times do + RubyVM::InstructionSequence.load_from_binary(a) + end + end; + end + + def test_ibf_bignum + iseq = RubyVM::InstructionSequence.compile("0x0"+"_0123_4567_89ab_cdef"*5) + expected = iseq.eval + result = RubyVM::InstructionSequence.load_from_binary(iseq_to_binary(iseq)).eval + assert_equal expected, result, proc {sprintf("expected: %x, result: %x", expected, result)} + end + + def test_compile_prism_with_file + Tempfile.create(%w"test_iseq .rb") do |f| + f.puts "_name = 'Prism'; puts 'hello'" + f.close + + assert_nothing_raised(TypeError) do + RubyVM::InstructionSequence.compile_prism(f) + end + end + end + + def block_using_method + yield + end + + def block_unused_method + end + + def test_unused_param + a = RubyVM::InstructionSequence.of(method(:block_using_method)).to_a + + assert_equal true, a.dig(11, :use_block) + + b = RubyVM::InstructionSequence.of(method(:block_unused_method)).to_a + assert_equal nil, b.dig(11, :use_block) + end + + def test_compile_prism_with_invalid_object_type + assert_raise(TypeError) do + RubyVM::InstructionSequence.compile_prism(Object.new) + end + end + + def test_load_from_binary_only_accepts_string_param + assert_raise(TypeError) do + var_0 = 0 + RubyVM::InstructionSequence.load_from_binary(var_0) + end + end + + def test_while_in_until_condition + assert_in_out_err(["--dump=i", "-e", "until while 1; end; end"]) do |stdout, stderr, status| + assert_include(stdout.shift, "== disasm:") + assert_include(stdout.pop, "leave") + assert_predicate(status, :success?) + end + end + + def test_compile_empty_under_gc_stress + EnvUtil.under_gc_stress do + RubyVM::InstructionSequence.compile_file(File::NULL) + end end end diff --git a/test/ruby/test_iterator.rb b/test/ruby/test_iterator.rb index 820d5591c1..1bb655d52e 100644 --- a/test/ruby/test_iterator.rb +++ b/test/ruby/test_iterator.rb @@ -175,10 +175,13 @@ class TestIterator < Test::Unit::TestCase end def test_block_given + verbose_bak, $VERBOSE = $VERBOSE, nil assert(m1{p 'test'}) assert(m2{p 'test'}) assert(!m1()) assert(!m2()) + ensure + $VERBOSE = verbose_bak end def m3(var, &block) @@ -308,7 +311,18 @@ class TestIterator < Test::Unit::TestCase def test_ljump assert_raise(LocalJumpError) {get_block{break}.call} - assert_raise(LocalJumpError) {proc_call2(get_block{break}){}} + begin + verbose_bak, $VERBOSE = $VERBOSE, nil + # See the commit https://github.com/ruby/ruby/commit/7d8a415bc2d08a1b5e9d1ea802493b6eeb99c219 + # This block is not used but this is intentional. + # | + # +-----------------------------------------------------+ + # | + # vv + assert_raise(LocalJumpError) {proc_call2(get_block{break}){}} + ensure + $VERBOSE = verbose_bak + end # cannot use assert_nothing_raised due to passing block. begin diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb index ef594bd52e..c836abd0c6 100644 --- a/test/ruby/test_keyword.rb +++ b/test/ruby/test_keyword.rb @@ -182,6 +182,52 @@ class TestKeywordArguments < Test::Unit::TestCase [:keyrest, :kw], [:block, :b]], method(:f9).parameters) end + def test_keyword_with_anonymous_keyword_splat + def self.a(b: 1, **) [b, **] end + kw = {b: 2, c: 3} + assert_equal([2, {c: 3}], a(**kw)) + assert_equal({b: 2, c: 3}, kw) + end + + def test_keyword_splat_nil + # cfunc call + assert_equal(nil, p(**nil)) + + def self.a0(&); end + assert_equal(nil, a0(**nil)) + assert_equal(nil, :a0.to_proc.call(self, **nil)) + assert_equal(nil, a0(**nil, &:block)) + + def self.o(x=1); x end + assert_equal(1, o(**nil)) + assert_equal(2, o(2, **nil)) + assert_equal(1, o(*nil, **nil)) + assert_equal(1, o(**nil, **nil)) + assert_equal({a: 1}, o(a: 1, **nil)) + assert_equal({a: 1}, o(**nil, a: 1)) + + # symproc call + assert_equal(1, :o.to_proc.call(self, **nil)) + + def self.s(*a); a end + assert_equal([], s(**nil)) + assert_equal([1], s(1, **nil)) + assert_equal([], s(*nil, **nil)) + + def self.kws(**a); a end + assert_equal({}, kws(**nil)) + assert_equal({}, kws(*nil, **nil)) + + def self.skws(*a, **kw); [a, kw] end + assert_equal([[], {}], skws(**nil)) + assert_equal([[1], {}], skws(1, **nil)) + assert_equal([[], {}], skws(*nil, **nil)) + + assert_equal({}, {**nil}) + assert_equal({a: 1}, {a: 1, **nil}) + assert_equal({a: 1}, {**nil, a: 1}) + end + def test_lambda f = ->(str: "foo", num: 424242) { [str, num] } assert_equal(["foo", 424242], f[]) @@ -237,16 +283,16 @@ class TestKeywordArguments < Test::Unit::TestCase assert_equal(true, Hash.ruby2_keywords_hash?(marked)) end + def assert_equal_not_same(kw, res) + assert_instance_of(Hash, res) + assert_equal(kw, res) + assert_not_same(kw, res) + end + def test_keyword_splat_new kw = {} h = {a: 1} - def self.assert_equal_not_same(kw, res) - assert_instance_of(Hash, res) - assert_equal(kw, res) - assert_not_same(kw, res) - end - def self.yo(**kw) kw end m = method(:yo) assert_equal(false, yo(**{}).frozen?) @@ -459,6 +505,20 @@ class TestKeywordArguments < Test::Unit::TestCase assert_equal_not_same h, yo(*a, **h) end + def test_keyword_splat_to_non_keyword_method + h = {a: 1}.freeze + + def self.yo(kw) kw end + assert_equal_not_same(h, yo(**h)) + assert_equal_not_same(h, method(:yo).(**h)) + assert_equal_not_same(h, :yo.to_proc.(self, **h)) + + def self.yoa(*kw) kw[0] end + assert_equal_not_same(h, yoa(**h)) + assert_equal_not_same(h, method(:yoa).(**h)) + assert_equal_not_same(h, :yoa.to_proc.(self, **h)) + end + def test_regular_kwsplat kw = {} h = {:a=>1} @@ -2364,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)} @@ -2376,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 @@ -2730,10 +2805,27 @@ 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 + assert_nil(c.send(:ruby2_keywords, utf16_sym)) + end + o = Object.new class << o alias bar p @@ -2757,8 +2849,53 @@ class TestKeywordArguments < Test::Unit::TestCase assert_raise(FrozenError) { c.send(:ruby2_keywords, :baz) } end + def test_anon_splat_ruby2_keywords + singleton_class.class_exec do + def bar(*a, **kw) + [a, kw] + end + + ruby2_keywords def bar_anon(*) + bar(*) + end + end + + a = [1, 2] + kw = {a: 1} + assert_equal([[1, 2], {a: 1}], bar_anon(*a, **kw)) + assert_equal([1, 2], a) + assert_equal({a: 1}, kw) + end + + def test_anon_splat_ruby2_keywords_bug_20388 + extend(Module.new{def process(action, ...) 1 end}) + extend(Module.new do + def process(action, *args) + args.freeze + super + end + ruby2_keywords :process + end) + + assert_equal(1, process(:foo, bar: :baz)) + end + + def test_ruby2_keywords_bug_20679 + c = Class.new do + def self.get(_, _, h, &block) + h[1] + end + + ruby2_keywords def get(*args, &block) + self.class.get(*args, &block) + end + end + + assert_equal 2, c.new.get(true, {}, 1 => 2) + end + def test_top_ruby2_keywords - assert_in_out_err([], <<-INPUT, ["[1, 2, 3]", "{:k=>1}"], []) + assert_in_out_err([], <<-INPUT, ["[1, 2, 3]", "{k: 1}"], []) def bar(*a, **kw) p a, kw end @@ -3922,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 @@ -3932,6 +4069,20 @@ class TestKeywordArguments < Test::Unit::TestCase }, bug8964 end + def test_large_kwsplat_to_method_taking_kw_and_kwsplat + assert_separately(['-'], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + n = 100000 + x = Fiber.new do + h = {kw: 2} + n.times{|i| h[i.to_s.to_sym] = i} + def self.f(kw: 1, **kws) kws.size end + f(**h) + end.resume + assert_equal(n, x) + end; + end + def test_dynamic_symbol_keyword bug10266 = '[ruby-dev:48564] [Bug #10266]' assert_separately(['-', bug10266], "#{<<~"begin;"}\n#{<<~'end;'}") @@ -4418,6 +4569,24 @@ class TestKeywordArgumentsSymProcRefinements < Test::Unit::TestCase assert_equal({one: 1, two: 2}, f.call(one:, two:)) end + def m_bug20570(*a, **nil) + a + end + + def test_splat_arg_with_prohibited_keyword + assert_equal([], m_bug20570(*[])) + assert_equal([1], m_bug20570(*[1])) + assert_equal([1, 2], m_bug20570(*[1, 2])) + h = nil + assert_equal([], m_bug20570(*[], **h)) + assert_equal([1], m_bug20570(*[1], **h)) + assert_equal([1, 2], m_bug20570(*[1, 2], **h)) + + assert_equal([], m_bug20570(*[], **nil)) + assert_equal([1], m_bug20570(*[1], **nil)) + assert_equal([1, 2], m_bug20570(*[1, 2], **nil)) + end + private def one 1 end diff --git a/test/ruby/test_lambda.rb b/test/ruby/test_lambda.rb index 9949fab8c7..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 @@ -177,32 +177,6 @@ class TestLambdaParameters < Test::Unit::TestCase RUBY end - def pass_along(&block) - lambda(&block) - end - - def pass_along2(&block) - pass_along(&block) - end - - def test_create_non_lambda_for_proc_one_level - prev_warning, Warning[:deprecated] = Warning[:deprecated], false - f = pass_along {} - refute_predicate(f, :lambda?, '[Bug #15620]') - assert_nothing_raised(ArgumentError) { f.call(:extra_arg) } - ensure - Warning[:deprecated] = prev_warning - end - - def test_create_non_lambda_for_proc_two_levels - prev_warning, Warning[:deprecated] = Warning[:deprecated], false - f = pass_along2 {} - refute_predicate(f, :lambda?, '[Bug #15620]') - assert_nothing_raised(ArgumentError) { f.call(:extra_arg) } - ensure - Warning[:deprecated] = prev_warning - end - def test_instance_exec bug12568 = '[ruby-core:76300] [Bug #12568]' assert_nothing_raised(ArgumentError, bug12568) do @@ -302,27 +276,27 @@ class TestLambdaParameters < Test::Unit::TestCase end def test_do_lambda_source_location - exp_lineno = __LINE__ + 3 + exp = [__LINE__ + 1, 10, __LINE__ + 5, 7] lmd = ->(x, y, z) do # end - file, lineno = lmd.source_location + file, *loc = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp_lineno, lineno, "must be at the beginning of the block") + assert_equal(exp, loc) end def test_brace_lambda_source_location - exp_lineno = __LINE__ + 3 + exp = [__LINE__ + 1, 10, __LINE__ + 5, 5] lmd = ->(x, y, z) { # } - file, lineno = lmd.source_location + file, *loc = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp_lineno, lineno, "must be at the beginning of the block") + assert_equal(exp, loc) end def test_not_orphan_return diff --git a/test/ruby/test_lazy_enumerator.rb b/test/ruby/test_lazy_enumerator.rb index 32bf097318..4dddbab50c 100644 --- a/test/ruby/test_lazy_enumerator.rb +++ b/test/ruby/test_lazy_enumerator.rb @@ -282,6 +282,11 @@ class TestLazyEnumerator < Test::Unit::TestCase assert_equal(3, a.current) end + def test_zip_map_lambda_bug_19569 + ary = [1, 2, 3].to_enum.lazy.zip([:a, :b, :c]).map(&:last).to_a + assert_equal([:a, :b, :c], ary) + end + def test_take a = Step.new(1..10) assert_equal(1, a.take(5).first) @@ -484,6 +489,10 @@ EOS assert_equal [1, 2, 3], enum.map { |x| x / 2 } end + def test_lazy_zip_map_yield_arity_bug_20623 + assert_equal([[1, 2]], [1].lazy.zip([2].lazy).map { |x| x }.force) + end + def test_lazy_to_enum lazy = [1, 2, 3].lazy def lazy.foo(*args) diff --git a/test/ruby/test_literal.rb b/test/ruby/test_literal.rb index 99dd3a0c56..cff888d4b3 100644 --- a/test/ruby/test_literal.rb +++ b/test/ruby/test_literal.rb @@ -39,6 +39,8 @@ class TestRubyLiteral < Test::Unit::TestCase end def test_string + verbose_bak, $VERBOSE = $VERBOSE, nil # prevent syntax warnings + assert_instance_of String, ?a assert_equal "a", ?a assert_instance_of String, ?A @@ -94,6 +96,15 @@ class TestRubyLiteral < Test::Unit::TestCase assert_equal "ab", eval("?a 'b'") assert_equal "a\nb", eval("<<A 'b'\na\nA") + + assert_raise(SyntaxError) {eval('"\C-' "\u3042" '"')} + assert_raise(SyntaxError) {eval('"\C-\\' "\u3042" '"')} + assert_raise(SyntaxError) {eval('"\M-' "\u3042" '"')} + assert_raise(SyntaxError) {eval('"\M-\\' "\u3042" '"')} + + assert_equal "\x09 \xC9 \x89", eval('"\C-\111 \M-\111 \M-\C-\111"') + ensure + $VERBOSE = verbose_bak end def test_dstring @@ -142,6 +153,8 @@ class TestRubyLiteral < Test::Unit::TestCase end def test_frozen_string + default = eval("'test'").frozen? + all_assertions do |a| a.for("false with indicator") do str = eval("# -*- frozen-string-literal: false -*-\n""'foo'") @@ -161,19 +174,19 @@ class TestRubyLiteral < Test::Unit::TestCase end a.for("false with preceding garbage") do str = eval("# x frozen-string-literal: false\n""'foo'") - assert_not_predicate(str, :frozen?) + assert_equal(default, str.frozen?) end a.for("true with preceding garbage") do str = eval("# x frozen-string-literal: true\n""'foo'") - assert_not_predicate(str, :frozen?) + assert_equal(default, str.frozen?) end a.for("false with succeeding garbage") do str = eval("# frozen-string-literal: false x\n""'foo'") - assert_not_predicate(str, :frozen?) + assert_equal(default, str.frozen?) end a.for("true with succeeding garbage") do str = eval("# frozen-string-literal: true x\n""'foo'") - assert_not_predicate(str, :frozen?) + assert_equal(default, str.frozen?) end end end @@ -184,6 +197,11 @@ class TestRubyLiteral < Test::Unit::TestCase list.each { |str| assert_predicate str, :frozen? } end + def test_string_in_hash_literal + hash = eval("# frozen-string-literal: false\n""{foo: 'foo'}") + assert_not_predicate(hash[:foo], :frozen?) + end + if defined?(RubyVM::InstructionSequence.compile_option) and RubyVM::InstructionSequence.compile_option.key?(:debug_frozen_string_literal) def test_debug_frozen_string @@ -234,8 +252,9 @@ class TestRubyLiteral < Test::Unit::TestCase def test_dregexp assert_instance_of Regexp, /re#{'ge'}xp/ assert_equal(/regexp/, /re#{'ge'}xp/) - bug3903 = '[ruby-core:32682]' - assert_raise(SyntaxError, bug3903) {eval('/[#{"\x80"}]/')} + + # [ruby-core:32682] + eval('/[#{"\x80"}]/') end def test_array @@ -496,10 +515,11 @@ class TestRubyLiteral < Test::Unit::TestCase '1.0i', '1.72723e-77', '//', + '__LINE__', + '__FILE__', + '__ENCODING__', ) do |key| - assert_warning(/key #{Regexp.quote(eval(key).inspect)} is duplicated/) do - eval("{#{key} => :bar, #{key} => :foo}") - end + assert_warning(/key #{Regexp.quote(eval(key).inspect)} is duplicated/) { eval("{#{key} => :bar, #{key} => :foo}") } end end @@ -582,6 +602,8 @@ class TestRubyLiteral < Test::Unit::TestCase end def test_integer + verbose_bak, $VERBOSE = $VERBOSE, nil # prevent syntax warnings + head = ['', '0x', '0o', '0b', '0d', '-', '+'] chars = ['0', '1', '_', '9', 'f'] head.each {|h| @@ -611,9 +633,14 @@ class TestRubyLiteral < Test::Unit::TestCase assert_syntax_error(h, /numeric literal without digits\Z/, "#{bug2407}: #{h.inspect}") end end + + ensure + $VERBOSE = verbose_bak end def test_float + verbose_bak, $VERBOSE = $VERBOSE, nil # prevent syntax warnings + head = ['', '-', '+'] chars = ['0', '1', '_', '9', 'f', '.'] head.each {|h| @@ -632,15 +659,32 @@ class TestRubyLiteral < Test::Unit::TestCase end begin r2 = eval(s) - rescue NameError, SyntaxError + rescue ArgumentError + # Debug log for a random failure: ArgumentError: SyntaxError#path changed + $stderr.puts "TestRubyLiteral#test_float failed: %p" % s + raise + rescue SyntaxError => e + r2 = :err + rescue NameError r2 = :err end r2 = :err if Range === r2 - assert_equal(r1, r2, "Float(#{s.inspect}) != eval(#{s.inspect})") + s = s.inspect + mesg = "Float(#{s}) != eval(#{s})" + mesg << ":" << e.message if e + assert_equal(r1, r2, mesg) } } } assert_equal(100.0, 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100e100) + + ensure + $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 diff --git a/test/ruby/test_m17n.rb b/test/ruby/test_m17n.rb index 28293ffffc..9f7a3c7f4b 100644 --- a/test/ruby/test_m17n.rb +++ b/test/ruby/test_m17n.rb @@ -69,15 +69,6 @@ class TestM17N < Test::Unit::TestCase assert_regexp_fixed_encoding(r) end - def assert_regexp_usascii_literal(r, enc, ex = nil) - code = "# -*- encoding: US-ASCII -*-\n#{r}.encoding" - if ex - assert_raise(ex) { eval(code) } - else - assert_equal(enc, eval(code)) - end - end - def encdump(str) d = str.dump if /\.force_encoding\("[A-Za-z0-9.:_+-]*"\)\z/ =~ d @@ -195,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 @@ -255,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 @@ -1090,7 +1067,23 @@ class TestM17N < Test::Unit::TestCase assert_nil(e("\xa1\xa2\xa3\xa4").index(e("\xa3"))) assert_nil(e("\xa1\xa2\xa3\xa4").rindex(e("\xa3"))) s = e("\xa3\xb0\xa3\xb1\xa3\xb2\xa3\xb3\xa3\xb4") - assert_raise(Encoding::CompatibilityError){s.rindex(a("\xb1\xa3"))} + + a_with_e = /EUC-JP and BINARY \(ASCII-8BIT\)/ + assert_raise_with_message(Encoding::CompatibilityError, a_with_e) do + s.index(a("\xb1\xa3")) + end + assert_raise_with_message(Encoding::CompatibilityError, a_with_e) do + s.rindex(a("\xb1\xa3")) + end + + a_with_e = /BINARY \(ASCII-8BIT\) regexp with EUC-JP string/ + assert_raise_with_message(Encoding::CompatibilityError, a_with_e) do + s.index(Regexp.new(a("\xb1\xa3"))) + end + assert_raise_with_message(Encoding::CompatibilityError, a_with_e) do + s.rindex(Regexp.new(a("\xb1\xa3"))) + end + bug11488 = '[ruby-core:70592] [Bug #11488]' each_encoding("abcdef", "def") do |str, substr| assert_equal(3, str.index(substr), bug11488) @@ -1420,31 +1413,42 @@ class TestM17N < Test::Unit::TestCase end def test_regexp_usascii - assert_regexp_usascii_literal('//', Encoding::US_ASCII) - assert_regexp_usascii_literal('/#{ }/', Encoding::US_ASCII) - assert_regexp_usascii_literal('/#{"a"}/', Encoding::US_ASCII) - assert_regexp_usascii_literal('/#{%q"\x80"}/', Encoding::ASCII_8BIT) - assert_regexp_usascii_literal('/#{"\x80"}/', nil, SyntaxError) - - assert_regexp_usascii_literal('/a/', Encoding::US_ASCII) - assert_regexp_usascii_literal('/a#{ }/', Encoding::US_ASCII) - assert_regexp_usascii_literal('/a#{"a"}/', Encoding::US_ASCII) - assert_regexp_usascii_literal('/a#{%q"\x80"}/', Encoding::ASCII_8BIT) - assert_regexp_usascii_literal('/a#{"\x80"}/', nil, SyntaxError) - - assert_regexp_usascii_literal('/\x80/', Encoding::ASCII_8BIT) - assert_regexp_usascii_literal('/\x80#{ }/', Encoding::ASCII_8BIT) - assert_regexp_usascii_literal('/\x80#{"a"}/', Encoding::ASCII_8BIT) - assert_regexp_usascii_literal('/\x80#{%q"\x80"}/', Encoding::ASCII_8BIT) - assert_regexp_usascii_literal('/\x80#{"\x80"}/', nil, SyntaxError) - - assert_regexp_usascii_literal('/\u1234/', Encoding::UTF_8) - assert_regexp_usascii_literal('/\u1234#{ }/', Encoding::UTF_8) - assert_regexp_usascii_literal('/\u1234#{"a"}/', Encoding::UTF_8) - assert_regexp_usascii_literal('/\u1234#{%q"\x80"}/', nil, SyntaxError) - assert_regexp_usascii_literal('/\u1234#{"\x80"}/', nil, SyntaxError) - assert_regexp_usascii_literal('/\u1234\x80/', nil, SyntaxError) - assert_regexp_usascii_literal('/\u1234#{ }\x80/', nil, RegexpError) + tests = [ + [__LINE__, '//', Encoding::US_ASCII], + [__LINE__, '/#{ }/', Encoding::US_ASCII], + [__LINE__, '/#{"a"}/', Encoding::US_ASCII], + [__LINE__, '/#{%q"\x80"}/', Encoding::US_ASCII], + [__LINE__, '/#{"\x80"}/', Encoding::ASCII_8BIT], + + [__LINE__, '/a/', Encoding::US_ASCII], + [__LINE__, '/a#{ }/', Encoding::US_ASCII], + [__LINE__, '/a#{"a"}/', Encoding::US_ASCII], + [__LINE__, '/a#{%q"\x80"}/', Encoding::ASCII_8BIT], + [__LINE__, '/a#{"\x80"}/', Encoding::ASCII_8BIT], + + [__LINE__, '/\x80/', Encoding::ASCII_8BIT], + [__LINE__, '/\x80#{ }/', Encoding::ASCII_8BIT], + [__LINE__, '/\x80#{"a"}/', Encoding::ASCII_8BIT], + [__LINE__, '/\x80#{%q"\x80"}/', Encoding::ASCII_8BIT], + [__LINE__, '/\x80#{"\x80"}/', Encoding::ASCII_8BIT], + + [__LINE__, '/\u1234/', Encoding::UTF_8], + [__LINE__, '/\u1234#{ }/', Encoding::UTF_8], + [__LINE__, '/\u1234#{"a"}/', Encoding::UTF_8], + + [__LINE__, '/\u1234#{%q"\x80"}/', nil, SyntaxError], + [__LINE__, '/\u1234#{"\x80"}/', nil, SyntaxError], + [__LINE__, '/\u1234\x80/', nil, SyntaxError], + [__LINE__, '/\u1234#{ }\x80/', nil, RegexpError], + ] + all_assertions_foreach(nil, *tests) do |line, r, enc, ex| + code = "# -*- encoding: US-ASCII -*-\n#{r}.encoding" + if ex + assert_raise(ex) {eval(code, nil, __FILE__, line-1)} + else + assert_equal(enc, eval(code, nil, __FILE__, line-1)) + end + end end def test_gbk @@ -1705,6 +1709,23 @@ class TestM17N < Test::Unit::TestCase assert_equal(e("[\"\xB4\xC1\xBB\xFA\"]"), s, bug11787) end + def test_encoding_names_of_default_internal + # [Bug #20595] [Bug #20598] + [ + "default_internal.names", + "name_list", + "aliases.keys" + ].each do |method| + assert_separately(%w(-W0), <<~RUBY) + exp_name = "int" + "ernal" + Encoding.default_internal = Encoding::ASCII_8BIT + name = Encoding.#{method}.find { |x| x == exp_name } + Encoding.default_internal = nil + assert_equal exp_name, name, "Encoding.#{method} [Bug #20595] [Bug #20598]" + RUBY + end + end + def test_greek_capital_gap bug12204 = '[ruby-core:74478] [Bug #12204] GREEK CAPITAL RHO and SIGMA' assert_equal("\u03A3", "\u03A1".succ, bug12204) diff --git a/test/ruby/test_marshal.rb b/test/ruby/test_marshal.rb index 6cd0b9acc3..eb66994801 100644 --- a/test/ruby/test_marshal.rb +++ b/test/ruby/test_marshal.rb @@ -1,6 +1,5 @@ # frozen_string_literal: false require 'test/unit' -require 'tempfile' require_relative 'marshaltestlib' class TestMarshal < Test::Unit::TestCase @@ -269,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]') @@ -302,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")) @@ -314,11 +323,10 @@ class TestMarshal < Test::Unit::TestCase assert_equal(c, Marshal.load(Marshal.dump(c)), bug2109) assert_nothing_raised(ArgumentError, '[ruby-dev:40386]') do - re = Tempfile.create("marshal_regexp") do |f| - f.binmode.write("\x04\bI/\x00\x00\x06:\rencoding\"\rUS-ASCII") - f.rewind - re2 = Marshal.load(f) - re2 + re = IO.pipe do |r, w| + w.write("\x04\bI/\x00\x00\x06:\rencoding\"\rUS-ASCII") + # Marshal.load would not overread and block + Marshal.load(r) end assert_equal(//, re) end @@ -461,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")} @@ -572,13 +604,19 @@ class TestMarshal < Test::Unit::TestCase def test_class_ivar assert_raise(TypeError) {Marshal.load("\x04\x08Ic\x1bTestMarshal::TestClass\x06:\x0e@ivar_bug\"\x08bug")} assert_raise(TypeError) {Marshal.load("\x04\x08IM\x1bTestMarshal::TestClass\x06:\x0e@ivar_bug\"\x08bug")} - assert_not_operator(TestClass, :instance_variable_defined?, :@bug) + assert_not_operator(TestClass, :instance_variable_defined?, :@ivar_bug) + + assert_raise(TypeError) {Marshal.load("\x04\x08[\x07c\x1bTestMarshal::TestClassI@\x06\x06:\x0e@ivar_bug\"\x08bug")} + assert_not_operator(TestClass, :instance_variable_defined?, :@ivar_bug) end def test_module_ivar assert_raise(TypeError) {Marshal.load("\x04\x08Im\x1cTestMarshal::TestModule\x06:\x0e@ivar_bug\"\x08bug")} assert_raise(TypeError) {Marshal.load("\x04\x08IM\x1cTestMarshal::TestModule\x06:\x0e@ivar_bug\"\x08bug")} - assert_not_operator(TestModule, :instance_variable_defined?, :@bug) + assert_not_operator(TestModule, :instance_variable_defined?, :@ivar_bug) + + assert_raise(TypeError) {Marshal.load("\x04\x08[\x07m\x1cTestMarshal::TestModuleI@\x06\x06:\x0e@ivar_bug\"\x08bug")} + assert_not_operator(TestModule, :instance_variable_defined?, :@ivar_bug) end class TestForRespondToFalse @@ -611,6 +649,8 @@ class TestMarshal < Test::Unit::TestCase def test_continuation EnvUtil.suppress_warning {require "continuation"} + omit 'requires callcc support' unless respond_to?(:callcc) + c = Bug9523.new assert_raise_with_message(RuntimeError, /Marshal\.dump reentered at marshal_dump/) do Marshal.dump(c) @@ -647,10 +687,10 @@ class TestMarshal < Test::Unit::TestCase Marshal.load(d) } - # cleanup + ensure self.class.class_eval do remove_const name - end + end if c end def test_unloadable_userdef @@ -664,9 +704,17 @@ class TestMarshal < Test::Unit::TestCase Marshal.load(d) } - # cleanup + ensure self.class.class_eval do remove_const name + end if c + end + + def test_recursive_userdef + t = Time.utc(0) + t.instance_eval {@v = t} + assert_raise_with_message(RuntimeError, /recursive\b.*\b_dump/) do + Marshal.dump(t) end end diff --git a/test/ruby/test_math.rb b/test/ruby/test_math.rb index 73f44c6ae3..e134600cc4 100644 --- a/test/ruby/test_math.rb +++ b/test/ruby/test_math.rb @@ -5,6 +5,7 @@ class TestMath < Test::Unit::TestCase def assert_infinity(a, *rest) rest = ["not infinity: #{a.inspect}"] if rest.empty? assert_predicate(a, :infinite?, *rest) + assert_predicate(a, :positive?, *rest) end def assert_nan(a, *rest) @@ -146,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)) @@ -165,6 +173,9 @@ class TestMath < Test::Unit::TestCase assert_nothing_raised { assert_nan(Math.log(0.0, 0.0)) } assert_nothing_raised { assert_nan(Math.log(Float::NAN)) } assert_nothing_raised { assert_nan(Math.log(1.0, Float::NAN)) } + assert_nothing_raised { assert_infinity(-Math.log(0)) } + assert_nothing_raised { assert_infinity(-Math.log(0, 2)) } + check(307.95368556425274, Math.log(2**1023, 10)) end def test_log2 @@ -179,6 +190,7 @@ class TestMath < Test::Unit::TestCase assert_raise_with_message(Math::DomainError, /\blog2\b/) { Math.log2(-1.0) } assert_raise_with_message(Math::DomainError, /\blog2\b/) { Math.log2(-Float::EPSILON) } assert_nothing_raised { assert_nan(Math.log2(Float::NAN)) } + assert_nothing_raised { assert_infinity(-Math.log2(0)) } end def test_log10 @@ -193,6 +205,20 @@ class TestMath < Test::Unit::TestCase assert_raise_with_message(Math::DomainError, /\blog10\b/) { Math.log10(-1.0) } assert_raise_with_message(Math::DomainError, /\blog10\b/) { Math.log10(-Float::EPSILON) } assert_nothing_raised { assert_nan(Math.log10(Float::NAN)) } + 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 @@ -277,8 +303,7 @@ class TestMath < Test::Unit::TestCase assert_raise_with_message(Math::DomainError, /\bgamma\b/) { Math.gamma(-1.0) } x = Math.gamma(-0.0) mesg = "Math.gamma(-0.0) should be -INF" - assert_infinity(x, mesg) - assert_predicate(x, :negative?, mesg) + assert_infinity(-x, mesg) assert_nan(Math.gamma(Float::NAN)) end @@ -296,12 +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_predicate(x, :positive?, 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 a04666890e..8561f841a8 100644 --- a/test/ruby/test_method.rb +++ b/test/ruby/test_method.rb @@ -209,6 +209,27 @@ class TestMethod < Test::Unit::TestCase assert_kind_of(String, o.method(:foo).hash.to_s) end + def test_hash_does_not_change_after_compaction + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + # iseq backed method + assert_separately([], <<~RUBY) + def a; end + + # Need this method here because otherwise the iseq may be on the C stack + # which would get pinned and not move during compaction + def get_hash + method(:a).hash + end + + hash = get_hash + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_equal(hash, get_hash) + RUBY + end + def test_owner c = Class.new do def foo; end @@ -263,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 @@ -294,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 @@ -450,6 +476,18 @@ class TestMethod < Test::Unit::TestCase assert_equal(:bar, m.clone.bar) end + def test_clone_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + o = Object.new + def o.foo; :foo; end + m = o.method(:foo) + def m.bar; :bar; end + assert_equal(:foo, m.clone.call) + assert_equal(:bar, m.clone.bar) + end + end + def test_inspect o = Object.new def o.foo; end; line_no = __LINE__ @@ -771,6 +809,14 @@ class TestMethod < Test::Unit::TestCase assert_raise(NoMethodError) { (self).mv2 } assert_nothing_raised { self.mv3 } + class << (obj = Object.new) + private def [](x) x end + def mv1(x) self[x] end + def mv2(x) (self)[x] end + end + assert_nothing_raised { obj.mv1(0) } + assert_raise(NoMethodError) { obj.mv2(0) } + v = Visibility.new assert_equal('method', defined?(v.mv1)) @@ -920,6 +966,38 @@ class TestMethod < Test::Unit::TestCase assert_raise(NameError, bug14658) {o.singleton_method(:bar)} end + def test_singleton_method_included_or_prepended_bug_20620 + m = Module.new do + extend self + def foo = :foo + end + assert_equal(:foo, m.singleton_method(:foo).call) + assert_raise(NameError) {m.singleton_method(:puts)} + + sc = Class.new do + def t = :t + end + c = Class.new(sc) do + singleton_class.prepend(Module.new do + def bar = :bar + end) + extend(Module.new do + def quux = :quux + end) + def self.baz = :baz + end + assert_equal(:bar, c.singleton_method(:bar).call) + assert_equal(:baz, c.singleton_method(:baz).call) + assert_equal(:quux, c.singleton_method(:quux).call) + + assert_raise(NameError) {c.singleton_method(:t)} + + c2 = Class.new(c) + assert_raise(NameError) {c2.singleton_method(:bar)} + assert_raise(NameError) {c2.singleton_method(:baz)} + assert_raise(NameError) {c2.singleton_method(:quux)} + end + Feature9783 = '[ruby-core:62212] [Feature #9783]' def assert_curry_three_args(m) @@ -1366,6 +1444,46 @@ class TestMethod < Test::Unit::TestCase def foo a = b = c = a = b = c = 12345 end + + def binding_noarg + a = a = 12345 + binding + end + + def binding_one_arg(x) + a = a = 12345 + binding + end + + def binding_optargs(x, y=42) + a = a = 12345 + binding + end + + def binding_anyargs(*x) + a = a = 12345 + binding + end + + def binding_keywords(x: 42) + a = a = 12345 + binding + end + + def binding_anykeywords(**x) + a = a = 12345 + binding + end + + def binding_forwarding(...) + a = a = 12345 + binding + end + + def binding_forwarding1(x, ...) + a = a = 12345 + binding + end end def test_to_proc_binding @@ -1384,6 +1502,66 @@ class TestMethod < Test::Unit::TestCase assert_equal([:bar, :foo], b.local_variables.sort, bug11012) end + def test_method_binding + c = C.new + + b = c.binding_noarg + assert_equal(12345, b.local_variable_get(:a)) + + b = c.binding_one_arg(0) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal(0, b.local_variable_get(:x)) + + b = c.binding_anyargs() + assert_equal(12345, b.local_variable_get(:a)) + assert_equal([], b.local_variable_get(:x)) + b = c.binding_anyargs(0) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal([0], b.local_variable_get(:x)) + b = c.binding_anyargs(0, 1) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal([0, 1], b.local_variable_get(:x)) + + b = c.binding_optargs(0) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal(0, b.local_variable_get(:x)) + assert_equal(42, b.local_variable_get(:y)) + b = c.binding_optargs(0, 1) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal(0, b.local_variable_get(:x)) + assert_equal(1, b.local_variable_get(:y)) + + b = c.binding_keywords() + assert_equal(12345, b.local_variable_get(:a)) + assert_equal(42, b.local_variable_get(:x)) + b = c.binding_keywords(x: 102) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal(102, b.local_variable_get(:x)) + + b = c.binding_anykeywords() + assert_equal(12345, b.local_variable_get(:a)) + assert_equal({}, b.local_variable_get(:x)) + b = c.binding_anykeywords(foo: 999) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal({foo: 999}, b.local_variable_get(:x)) + + b = c.binding_forwarding() + assert_equal(12345, b.local_variable_get(:a)) + b = c.binding_forwarding(0) + assert_equal(12345, b.local_variable_get(:a)) + b = c.binding_forwarding(0, 1) + assert_equal(12345, b.local_variable_get(:a)) + b = c.binding_forwarding(foo: 42) + assert_equal(12345, b.local_variable_get(:a)) + + b = c.binding_forwarding1(987) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal(987, b.local_variable_get(:x)) + b = c.binding_forwarding1(987, 654) + assert_equal(12345, b.local_variable_get(:a)) + assert_equal(987, b.local_variable_get(:x)) + end + MethodInMethodClass_Setup = -> do remove_const :MethodInMethodClass if defined? MethodInMethodClass @@ -1423,25 +1601,25 @@ class TestMethod < Test::Unit::TestCase end def test_argument_error_location - body = <<-'END_OF_BODY' - eval <<-'EOS' - $line_lambda = __LINE__; $f = lambda do - _x = 1 - end - $line_method = __LINE__; def foo - _x = 1 - end - begin - $f.call(1) - rescue ArgumentError => e - assert_equal "(eval):#{$line_lambda.to_s}:in `block in <main>'", e.backtrace.first - end - begin - foo(1) - rescue ArgumentError => e - assert_equal "(eval):#{$line_method}:in `foo'", e.backtrace.first - end - EOS + body = <<~'END_OF_BODY' + eval <<~'EOS', nil, "main.rb" + $line_lambda = __LINE__; $f = lambda do + _x = 1 + end + $line_method = __LINE__; def foo + _x = 1 + end + begin + $f.call(1) + rescue ArgumentError => e + assert_equal "main.rb:#{$line_lambda}:in 'block in <main>'", e.backtrace.first + end + begin + foo(1) + rescue ArgumentError => e + assert_equal "main.rb:#{$line_method}:in 'Object#foo'", e.backtrace.first + end + EOS END_OF_BODY assert_separately [], body @@ -1450,7 +1628,7 @@ class TestMethod < Test::Unit::TestCase end def test_zsuper_private_override_instance_method - assert_separately(%w(--disable-gems), <<-'end;', timeout: 30) + assert_separately([], <<-'end;', timeout: 30) # Bug #16942 [ruby-core:98691] module M def x @@ -1471,7 +1649,7 @@ class TestMethod < Test::Unit::TestCase end def test_override_optimized_method_on_class_using_prepend - assert_separately(%w(--disable-gems), <<-'end;', timeout: 30) + assert_separately([], <<-'end;', timeout: 30) # Bug #17725 [ruby-core:102884] $VERBOSE = nil String.prepend(Module.new) @@ -1595,4 +1773,121 @@ class TestMethod < Test::Unit::TestCase def test_invalidating_CC_ASAN assert_ruby_status(['-e', 'using Module.new']) end + + def test_kwarg_eval_memory_leak + assert_no_memory_leak([], "", <<~RUBY, rss: true, limit: 1.2) + obj = Object.new + def obj.test(**kwargs) = nil + + 100_000.times do + eval("obj.test(foo: 123)") + end + RUBY + end + + def test_super_with_splat + c = Class.new { + attr_reader :x + + def initialize(*args) + @x, _ = args + end + } + b = Class.new(c) { def initialize(...) = super } + a = Class.new(b) { def initialize(*args) = super } + obj = a.new(1, 2, 3) + assert_equal 1, obj.x + end + + def test_warn_unused_block + assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status| + def foo = nil + foo{} # warn + send(:foo){} # don't warn because it uses send + b = Proc.new{} + foo(&b) # warn + RUBY + errstr = err.join("\n") + assert_equal 2, err.size, errstr + + assert_match(/-:2: warning/, errstr) + assert_match(/-:5: warning/, errstr) + end + + assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status| + def foo = nil + 10.times{foo{}} # warn once + RUBY + assert_equal 1, err.size + end + + assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status| + def foo = nil; b = nil + foo(&b) # no warning + 1.object_id{} # no warning because it is written in C + + class C + def initialize + end + end + C.new{} # no warning + + RUBY + assert_equal 0, err.size + end + + assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status| + class C0 + def f1 = nil + def f2 = nil + def f3 = nil + def f4 = nil + def f5 = nil + def f6 = nil + end + + class C1 < C0 + def f1 = super # zsuper / use + def f2 = super() # super / use + def f3(&_) = super(&_) # super / use + def f4 = super(&nil) # super / unuse + def f5 = super(){} # super / unuse + def f6 = super{} # zsuper / unuse + end + + C1.new.f1{} # no warning + C1.new.f2{} # no warning + C1.new.f3{} # no warning + C1.new.f4{} # warning + C1.new.f5{} # warning + C1.new.f6{} # warning + RUBY + assert_equal 3, err.size, err.join("\n") + assert_match(/-:22: warning.+f4/, err.join) + assert_match(/-:23: warning.+f5/, err.join) + assert_match(/-:24: warning.+f6/, err.join) + end + + assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status| + class C0 + def f = yield + end + + class C1 < C0 + def f = nil + end + + C1.new.f{} # do not warn on duck typing + RUBY + assert_equal 0, err.size, err.join("\n") + end + + assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status| + def foo(*, &block) = block + def bar(buz, ...) = foo(buz, ...) + bar(:test) {} # do not warn because of forwarding + RUBY + assert_equal 0, err.size, err.join("\n") + end + end end diff --git a/test/ruby/test_mixed_unicode_escapes.rb b/test/ruby/test_mixed_unicode_escapes.rb index f0b4cc691f..a30b5c19f5 100644 --- a/test/ruby/test_mixed_unicode_escapes.rb +++ b/test/ruby/test_mixed_unicode_escapes.rb @@ -18,12 +18,12 @@ class TestMixedUnicodeEscape < Test::Unit::TestCase assert_raise(SyntaxError) { eval %q("\u{1234}é")} # also should not work for Regexp - assert_raise(SyntaxError) { eval %q(/#{"\u1234"}#{"é"}/)} assert_raise(RegexpError) { eval %q(/\u{1234}#{nil}é/)} assert_raise(RegexpError) { eval %q(/é#{nil}\u1234/)} # String interpolation turns into an expression and we get # a different kind of error, but we still can't mix these + assert_raise(Encoding::CompatibilityError) { eval %q(/#{"\u1234"}#{"é"}/)} assert_raise(Encoding::CompatibilityError) { eval %q("\u{1234}#{nil}é")} assert_raise(Encoding::CompatibilityError) { eval %q("é#{nil}\u1234")} end diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index f566eced3a..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 @@ -253,6 +253,14 @@ class TestModule < Test::Unit::TestCase assert_operator(Math, :const_defined?, "PI") assert_not_operator(Math, :const_defined?, :IP) assert_not_operator(Math, :const_defined?, "IP") + + # Test invalid symbol name + # [Bug #20245] + EnvUtil.under_gc_stress do + assert_raise(EncodingError) do + Math.const_defined?("\xC3") + end + end end def each_bad_constants(m, &b) @@ -404,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 @@ -427,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) @@ -593,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 @@ -776,6 +766,18 @@ class TestModule < Test::Unit::TestCase assert_equal([:m1, :m0, :m, :sc, :m1, :m0, :c], sc.new.m) end + def test_include_into_module_after_prepend_bug_20871 + bar = Module.new{def bar; 'bar'; end} + foo = Module.new{def foo; 'foo'; end} + m = Module.new + c = Class.new{include m} + m.prepend bar + Class.new{include m} + m.include foo + assert_include c.ancestors, foo + assert_equal "foo", c.new.foo + end + def test_protected_include_into_included_module m1 = Module.new do def other_foo(other) @@ -811,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 @@ -1271,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 @@ -1429,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 }) @@ -1437,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 @@ -1468,8 +1484,8 @@ class TestModule < Test::Unit::TestCase class << o; self; end.instance_eval { undef_method(:foo) } end - %w(object_id __send__ initialize).each do |n| - assert_in_out_err([], <<-INPUT, [], %r"warning: undefining `#{n}' may cause serious problems$") + %w(object_id __id__ __send__ initialize).each do |n| + assert_in_out_err([], <<-INPUT, [], %r"warning: undefining '#{n}' may cause serious problems$") $VERBOSE = false Class.new.instance_eval { undef_method(:#{n}) } INPUT @@ -2140,9 +2156,8 @@ class TestModule < Test::Unit::TestCase Warning[:deprecated] = false Class.new(c)::FOO end - assert_warn('') do - Warning[:deprecated] = false - c.class_eval "FOO" + assert_warn(/deprecated/) do + c.class_eval {remove_const "FOO"} end end @@ -2870,6 +2885,7 @@ class TestModule < Test::Unit::TestCase def test_invalid_attr %W[ + foo= foo? @foo @@foo @@ -3000,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 @@ -3057,7 +3073,7 @@ class TestModule < Test::Unit::TestCase end def test_prepend_gc - assert_separately [], %{ + assert_ruby_status [], %{ module Foo end class Object @@ -3154,6 +3170,19 @@ class TestModule < Test::Unit::TestCase end; end + def test_define_method_changes_visibility_with_existing_method_bug_19749 + c = Class.new do + def a; end + private def b; end + + define_method(:b, instance_method(:b)) + private + define_method(:a, instance_method(:a)) + end + assert_equal([:b], c.public_instance_methods(false)) + assert_equal([:a], c.private_instance_methods(false)) + end + def test_define_method_with_unbound_method # Passing an UnboundMethod to define_method succeeds if it is from an ancestor assert_nothing_raised do @@ -3174,7 +3203,6 @@ class TestModule < Test::Unit::TestCase end def test_redefinition_mismatch - omit "Investigating trunk-rjit failure on ci.rvm.jp" if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? m = Module.new m.module_eval "A = 1", __FILE__, line = __LINE__ e = assert_raise_with_message(TypeError, /is not a module/) { @@ -3276,6 +3304,109 @@ class TestModule < Test::Unit::TestCase assert_match(/::Foo$/, mod.name, '[Bug #14895]') end + def test_iclass_memory_leak + # [Bug #19550] + assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true) + code = proc do + mod = Module.new + Class.new do + include mod + end + end + 1_000.times(&code) + PREP + 3_000_000.times(&code) + CODE + end + + def test_complemented_method_entry_memory_leak + # [Bug #19894] [Bug #19896] + assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true) + code = proc do + $c = Class.new do + def foo; end + end + + $m = Module.new do + refine $c do + def foo; end + end + end + + Class.new do + using $m + + def initialize + o = $c.new + o.method(:foo).unbind + end + end.new + end + 1_000.times(&code) + PREP + 300_000.times(&code) + CODE + end + + def test_module_clone_memory_leak + # [Bug #19901] + assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true) + code = proc do + Module.new.clone + end + 1_000.times(&code) + PREP + 1_000_000.times(&code) + CODE + end + + def test_set_temporary_name + m = Module.new + assert_nil m.name + + m.const_set(:N, Module.new) + + assert_match(/\A#<Module:0x\h+>::N\z/, m::N.name) + 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!} + assert_same m::N, m::N.set_temporary_name(nil) + assert_nil(m::N.name) + + m::N.const_set(:O, Module.new) + m.const_set(:Recursive, m) + m::N.const_set(:Recursive, m) + m.const_set(:A, 42) + + 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) + + assert_same m, m.set_temporary_name(nil) + assert_nil m.name + assert_nil m::N.name + assert_nil m::N::O.name + + assert_raise_with_message(ArgumentError, "empty class/module name") do + m.set_temporary_name("") + end + %w[A A::B ::A ::A::B].each do |name| + assert_raise_with_message(ArgumentError, /must not be a constant path/) do + m.set_temporary_name(name) + end + end + + [Object, User, AClass].each do |mod| + assert_raise_with_message(RuntimeError, /permanent name/) do + mod.set_temporary_name("fake_name") + end + end + end + private def assert_top_method_is_private(method) @@ -3283,7 +3414,7 @@ class TestModule < Test::Unit::TestCase methods = singleton_class.private_instance_methods(false) assert_include(methods, :#{method}, ":#{method} should be private") - assert_raise_with_message(NoMethodError, /^private method `#{method}' called for /) { + assert_raise_with_message(NoMethodError, /^private method '#{method}' called for /) { recv = self recv.#{method} } diff --git a/test/ruby/test_nomethod_error.rb b/test/ruby/test_nomethod_error.rb index 0306535943..6abd20cc81 100644 --- a/test/ruby/test_nomethod_error.rb +++ b/test/ruby/test_nomethod_error.rb @@ -78,14 +78,14 @@ 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 bug3237 = '[ruby-core:29948]' str = "\u2600" id = :"\u2604" - msg = "undefined method `#{id}' for an instance of String" + msg = "undefined method '#{id}' for an instance of String" assert_raise_with_message(NoMethodError, Regexp.compile(Regexp.quote(msg)), bug3237) do str.__send__(id) end @@ -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 26256b80db..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 @@ -355,6 +355,44 @@ class TestObject < Test::Unit::TestCase end end + def test_remove_instance_variable_re_embed + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + c = Class.new do + attr_reader :a, :b, :c + + def initialize + @a = nil + @b = nil + @c = nil + end + end + + o1 = c.new + o2 = c.new + + o1.instance_variable_set(:@foo, 5) + o1.instance_variable_set(:@a, 0) + o1.instance_variable_set(:@b, 1) + o1.instance_variable_set(:@c, 2) + refute_includes ObjectSpace.dump(o1), '"embedded":true' + o1.remove_instance_variable(:@foo) + assert_includes ObjectSpace.dump(o1), '"embedded":true' + + o2.instance_variable_set(:@a, 0) + o2.instance_variable_set(:@b, 1) + o2.instance_variable_set(:@c, 2) + assert_includes ObjectSpace.dump(o2), '"embedded":true' + + assert_equal(0, o1.a) + assert_equal(1, o1.b) + assert_equal(2, o1.c) + assert_equal(0, o2.a) + assert_equal(1, o2.b) + assert_equal(2, o2.c) + end; + end + def test_convert_string o = Object.new def o.to_s; 1; end @@ -422,6 +460,18 @@ class TestObject < Test::Unit::TestCase assert_equal(1+3+5+7+9, n) end + def test_max_shape_variation_with_performance_warnings + assert_in_out_err([], <<-INPUT, %w(), /The class Foo reached 8 shape variations, instance variables accesses will be slower and memory usage increased/) + $VERBOSE = false + Warning[:performance] = true + + class Foo; end + 10.times do |i| + Foo.new.instance_variable_set(:"@a\#{i}", nil) + end + INPUT + end + def test_redefine_method_under_verbose assert_in_out_err([], <<-INPUT, %w(2), /warning: method redefined; discarding old foo$/) $VERBOSE = true @@ -433,15 +483,30 @@ class TestObject < Test::Unit::TestCase end def test_redefine_method_which_may_case_serious_problem - assert_in_out_err([], <<-INPUT, [], %r"warning: redefining `object_id' may cause serious problems$") - $VERBOSE = false - def (Object.new).object_id; end - INPUT + %w(object_id __id__ __send__).each do |m| + assert_in_out_err([], <<-INPUT, [], %r"warning: redefining '#{m}' may cause serious problems$") + $VERBOSE = false + def (Object.new).#{m}; end + INPUT - assert_in_out_err([], <<-INPUT, [], %r"warning: redefining `__send__' may cause serious problems$") - $VERBOSE = false - def (Object.new).__send__; end - INPUT + assert_in_out_err([], <<-INPUT, [], %r"warning: redefining '#{m}' may cause serious problems$") + $VERBOSE = false + Class.new.define_method(:#{m}) {} + INPUT + + assert_in_out_err([], <<-INPUT, [], %r"warning: redefining '#{m}' may cause serious problems$") + $VERBOSE = false + Class.new.attr_reader(:#{m}) + INPUT + + assert_in_out_err([], <<-INPUT, [], %r"warning: redefining '#{m}' may cause serious problems$") + $VERBOSE = false + Class.new do + def foo; end + alias #{m} foo + end + INPUT + end bug10421 = '[ruby-dev:48691] [Bug #10421]' assert_in_out_err([], <<-INPUT, ["1"], [], bug10421) @@ -480,8 +545,8 @@ class TestObject < Test::Unit::TestCase bug2202 = '[ruby-core:26074]' assert_raise(NoMethodError, bug2202) {o2.meth2} - %w(object_id __send__ initialize).each do |m| - assert_in_out_err([], <<-INPUT, %w(:ok), %r"warning: removing `#{m}' may cause serious problems$") + %w(object_id __id__ __send__ initialize).each do |m| + assert_in_out_err([], <<-INPUT, %w(:ok), %r"warning: removing '#{m}' may cause serious problems$") $VERBOSE = false begin Class.new.instance_eval { remove_method(:#{m}) } @@ -888,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 a7cfb064a8..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,17 +57,20 @@ 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 - msg = /is not symbol id value/ - assert_raise_with_message(RangeError, msg) { ObjectSpace._id2ref(:a.object_id + GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]) } + # RB_STATIC_SYM_P checks for static symbols by checking that the bottom + # 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) { EnvUtil.suppress_warning { ObjectSpace._id2ref(:a.object_id + 256) } } end def test_count_objects @@ -91,13 +94,27 @@ 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 ObjectSpace.define_finalizer(a) { p :ok } !b END + + assert_in_out_err(["-e", <<~RUBY], "", %w(:ok :ok), [], timeout: 60) + a = Object.new + ObjectSpace.define_finalizer(a) { p :ok } + + 1_000_000.times do + o = Object.new + ObjectSpace.define_finalizer(o) { } + end + + b = Object.new + ObjectSpace.define_finalizer(b) { p :ok } + RUBY + assert_raise(ArgumentError) { ObjectSpace.define_finalizer([], Object.new) } code = proc do |priv| @@ -120,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 @@ -174,30 +210,28 @@ End end def test_finalizer_thread_raise - GC.disable - fzer = proc do |id| - sleep 0.2 - end - 2.times do - o = Object.new - ObjectSpace.define_finalizer(o, fzer) - end + EnvUtil.without_gc do + fzer = proc do |id| + sleep 0.2 + end + 2.times do + o = Object.new + ObjectSpace.define_finalizer(o, fzer) + end - my_error = Class.new(RuntimeError) - begin - main_th = Thread.current - Thread.new do - sleep 0.1 - main_th.raise(my_error) + my_error = Class.new(RuntimeError) + begin + main_th = Thread.current + Thread.new do + sleep 0.1 + main_th.raise(my_error) + end + GC.start + sleep(10) + assert(false) + rescue my_error end - GC.start - puts "After GC" - sleep(10) - assert(false) - rescue my_error end - ensure - GC.enable end def test_each_object @@ -250,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 9ce5371dd0..5d16984eef 100644 --- a/test/ruby/test_optimization.rb +++ b/test/ruby/test_optimization.rb @@ -16,6 +16,18 @@ class TestRubyOptimization < Test::Unit::TestCase end; end + def assert_performance_warning(klass, method) + assert_in_out_err([], "#{<<-"begin;"}\n#{<<~"end;"}", [], ["-:4: warning: Redefining '#{klass}##{method}' disables interpreter and JIT optimizations"]) + begin; + Warning[:performance] = true + class #{klass} + undef #{method} + def #{method} + end + end + end; + end + def disasm(name) RubyVM::InstructionSequence.of(method(name)).disasm end @@ -23,102 +35,122 @@ class TestRubyOptimization < Test::Unit::TestCase def test_fixnum_plus assert_equal 21, 10 + 11 assert_redefine_method('Integer', '+', 'assert_equal 11, 10 + 11') + assert_performance_warning('Integer', '+') end def test_fixnum_minus assert_equal 5, 8 - 3 assert_redefine_method('Integer', '-', 'assert_equal 3, 8 - 3') + assert_performance_warning('Integer', '-') end def test_fixnum_mul assert_equal 15, 3 * 5 assert_redefine_method('Integer', '*', 'assert_equal 5, 3 * 5') + assert_performance_warning('Integer', '*') end def test_fixnum_div assert_equal 3, 15 / 5 assert_redefine_method('Integer', '/', 'assert_equal 5, 15 / 5') + assert_performance_warning('Integer', '/') end def test_fixnum_mod assert_equal 1, 8 % 7 assert_redefine_method('Integer', '%', 'assert_equal 7, 8 % 7') + assert_performance_warning('Integer', '%') end def test_fixnum_lt assert_equal true, 1 < 2 assert_redefine_method('Integer', '<', 'assert_equal 2, 1 < 2') + assert_performance_warning('Integer', '<') end def test_fixnum_le assert_equal true, 1 <= 2 assert_redefine_method('Integer', '<=', 'assert_equal 2, 1 <= 2') + assert_performance_warning('Integer', '<=') end def test_fixnum_gt assert_equal false, 1 > 2 assert_redefine_method('Integer', '>', 'assert_equal 2, 1 > 2') + assert_performance_warning('Integer', '>') end def test_fixnum_ge assert_equal false, 1 >= 2 assert_redefine_method('Integer', '>=', 'assert_equal 2, 1 >= 2') + assert_performance_warning('Integer', '>=') end def test_float_plus assert_equal 4.0, 2.0 + 2.0 assert_redefine_method('Float', '+', 'assert_equal 2.0, 2.0 + 2.0') + assert_performance_warning('Float', '+') end def test_float_minus assert_equal 4.0, 2.0 + 2.0 - assert_redefine_method('Float', '+', 'assert_equal 2.0, 2.0 + 2.0') + assert_redefine_method('Float', '-', 'assert_equal 2.0, 4.0 - 2.0') + assert_performance_warning('Float', '-') end def test_float_mul assert_equal 29.25, 4.5 * 6.5 assert_redefine_method('Float', '*', 'assert_equal 6.5, 4.5 * 6.5') + assert_performance_warning('Float', '*') end def test_float_div assert_in_delta 0.63063063063063063, 4.2 / 6.66 assert_redefine_method('Float', '/', 'assert_equal 6.66, 4.2 / 6.66', "[Bug #9238]") + assert_performance_warning('Float', '/') end def test_float_lt assert_equal true, 1.1 < 2.2 assert_redefine_method('Float', '<', 'assert_equal 2.2, 1.1 < 2.2') + assert_performance_warning('Float', '<') end def test_float_le assert_equal true, 1.1 <= 2.2 assert_redefine_method('Float', '<=', 'assert_equal 2.2, 1.1 <= 2.2') + assert_performance_warning('Float', '<=') end def test_float_gt assert_equal false, 1.1 > 2.2 assert_redefine_method('Float', '>', 'assert_equal 2.2, 1.1 > 2.2') + assert_performance_warning('Float', '>') end def test_float_ge assert_equal false, 1.1 >= 2.2 assert_redefine_method('Float', '>=', 'assert_equal 2.2, 1.1 >= 2.2') + assert_performance_warning('Float', '>=') end def test_string_length assert_equal 6, "string".length assert_redefine_method('String', 'length', 'assert_nil "string".length') + assert_performance_warning('String', 'length') end def test_string_size assert_equal 6, "string".size assert_redefine_method('String', 'size', 'assert_nil "string".size') + assert_performance_warning('String', 'size') end def test_string_empty? assert_equal true, "".empty? assert_equal false, "string".empty? assert_redefine_method('String', 'empty?', 'assert_nil "string".empty?') + assert_performance_warning('String', 'empty?') end def test_string_plus @@ -127,39 +159,50 @@ class TestRubyOptimization < Test::Unit::TestCase assert_equal "x", "" + "x" assert_equal "ab", "a" + "b" assert_redefine_method('String', '+', 'assert_equal "b", "a" + "b"') + assert_performance_warning('String', '+') end def test_string_succ assert_equal 'b', 'a'.succ assert_equal 'B', 'A'.succ + assert_performance_warning('String', 'succ') end def test_string_format assert_equal '2', '%d' % 2 assert_redefine_method('String', '%', 'assert_equal 2, "%d" % 2') + assert_performance_warning('String', '%') end def test_string_freeze assert_equal "foo", "foo".freeze assert_equal "foo".freeze.object_id, "foo".freeze.object_id assert_redefine_method('String', 'freeze', 'assert_nil "foo".freeze') + assert_performance_warning('String', 'freeze') end def test_string_uminus assert_same "foo".freeze, -"foo" assert_redefine_method('String', '-@', 'assert_nil(-"foo")') + assert_performance_warning('String', '-@') end def test_array_min assert_equal 1, [1, 2, 4].min assert_redefine_method('Array', 'min', 'assert_nil([1, 2, 4].min)') assert_redefine_method('Array', 'min', 'assert_nil([1 + 0, 2, 4].min)') + assert_performance_warning('Array', 'min') end def test_array_max assert_equal 4, [1, 2, 4].max assert_redefine_method('Array', 'max', 'assert_nil([1, 2, 4].max)') assert_redefine_method('Array', 'max', 'assert_nil([1 + 0, 2, 4].max)') + assert_performance_warning('Array', 'max') + end + + def test_array_hash + assert_performance_warning('Array', 'hash') end def test_trace_optimized_methods @@ -235,6 +278,8 @@ class TestRubyOptimization < Test::Unit::TestCase assert_equal :b, (b #{m} "b").to_sym end end + + assert_performance_warning('String', '==') end def test_string_ltlt @@ -243,50 +288,59 @@ class TestRubyOptimization < Test::Unit::TestCase assert_equal "x", "" << "x" assert_equal "ab", "a" << "b" assert_redefine_method('String', '<<', 'assert_equal "b", "a" << "b"') + assert_performance_warning('String', '<<') end def test_fixnum_and assert_equal 1, 1&3 assert_redefine_method('Integer', '&', 'assert_equal 3, 1&3') + assert_performance_warning('Integer', '&') end def test_fixnum_or assert_equal 3, 1|3 assert_redefine_method('Integer', '|', 'assert_equal 1, 3|1') + assert_performance_warning('Integer', '|') end def test_array_plus assert_equal [1,2], [1]+[2] assert_redefine_method('Array', '+', 'assert_equal [2], [1]+[2]') + assert_performance_warning('Array', '+') end def test_array_minus assert_equal [2], [1,2] - [1] assert_redefine_method('Array', '-', 'assert_equal [1], [1,2]-[1]') + assert_performance_warning('Array', '-') end def test_array_length assert_equal 0, [].length assert_equal 3, [1,2,3].length assert_redefine_method('Array', 'length', 'assert_nil([].length); assert_nil([1,2,3].length)') + assert_performance_warning('Array', 'length') end def test_array_empty? assert_equal true, [].empty? assert_equal false, [1,2,3].empty? assert_redefine_method('Array', 'empty?', 'assert_nil([].empty?); assert_nil([1,2,3].empty?)') + assert_performance_warning('Array', 'empty?') end def test_hash_length assert_equal 0, {}.length assert_equal 1, {1=>1}.length assert_redefine_method('Hash', 'length', 'assert_nil({}.length); assert_nil({1=>1}.length)') + assert_performance_warning('Hash', 'length') end def test_hash_empty? assert_equal true, {}.empty? assert_equal false, {1=>1}.empty? assert_redefine_method('Hash', 'empty?', 'assert_nil({}.empty?); assert_nil({1=>1}.empty?)') + assert_performance_warning('Hash', 'empty?') end def test_hash_aref_with @@ -297,6 +351,7 @@ class TestRubyOptimization < Test::Unit::TestCase h = { "foo" => 1 } assert_equal "foo", h["foo"] end; + assert_performance_warning('Hash', '[]') end def test_hash_aset_with @@ -308,6 +363,7 @@ class TestRubyOptimization < Test::Unit::TestCase assert_equal 1, h["foo"] = 1, "assignment always returns value set" assert_nil h["foo"] end; + assert_performance_warning('Hash', '[]=') end class MyObj @@ -437,6 +493,31 @@ class TestRubyOptimization < Test::Unit::TestCase message(bug12565) {disasm(:add_one_and_two)}) end + def test_c_func_with_sp_offset_under_tailcall + tailcall("#{<<-"begin;"}\n#{<<~"end;"}") + begin; + def calc_one_plus_two + 1 + 2.abs + end + + def one_plus_two + calc_one_plus_two + end + end; + assert_equal(3, one_plus_two) + end + + def test_tailcall_and_post_arg + tailcall(<<~RUBY) + def ret_const = :ok + + def post_arg(_a = 1, _b) = ret_const + RUBY + + # YJIT probably uses a fallback on the call to post_arg + assert_equal(:ok, post_arg(0)) + end + def test_tailcall_interrupted_by_sigint bug12576 = 'ruby-core:76327' script = "#{<<-"begin;"}\n#{<<~'end;'}" @@ -510,7 +591,6 @@ class TestRubyOptimization < Test::Unit::TestCase end def test_tailcall_not_to_grow_stack - omit 'currently JIT-ed code always creates a new stack frame' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? bug16161 = '[ruby-core:94881]' tailcall("#{<<-"begin;"}\n#{<<~"end;"}") @@ -526,11 +606,11 @@ class TestRubyOptimization < Test::Unit::TestCase end class Bug10557 - def [](_) + def [](_, &) block_given? end - def []=(_, _) + def []=(_, _, &) block_given? end end @@ -552,7 +632,7 @@ class TestRubyOptimization < Test::Unit::TestCase begin; class String undef freeze - def freeze + def freeze(&) block_given? end end @@ -614,6 +694,7 @@ class TestRubyOptimization < Test::Unit::TestCase [ nil, true, false, 0.1, :sym, 'str', 0xffffffffffffffff ].each do |v| k = v.class.to_s assert_redefine_method(k, '===', "assert_equal(#{v.inspect} === 0, 0)") + assert_performance_warning(k, '===') end end @@ -667,6 +748,98 @@ class TestRubyOptimization < Test::Unit::TestCase end end + def test_peephole_array_freeze + code = "#{<<~'begin;'}\n#{<<~'end;'}" + begin; + [1].freeze + end; + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_match(/opt_ary_freeze/, insn) + assert_no_match(/duparray/, insn) + assert_no_match(/send/, insn) + assert_predicate([1].freeze, :frozen?) + assert_in_out_err([], <<~RUBY, [":ok"]) + class Array + prepend Module.new { + def freeze + :ok + end + } + end + p [1].freeze + RUBY + end + + def test_peephole_array_freeze_empty + code = "#{<<~'begin;'}\n#{<<~'end;'}" + begin; + [].freeze + end; + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_match(/opt_ary_freeze/, insn) + assert_no_match(/duparray/, insn) + assert_no_match(/send/, insn) + assert_predicate([].freeze, :frozen?) + assert_in_out_err([], <<~RUBY, [":ok"]) + class Array + prepend Module.new { + def freeze + :ok + end + } + end + p [].freeze + RUBY + end + + def test_peephole_hash_freeze + code = "#{<<~'begin;'}\n#{<<~'end;'}" + begin; + {a:1}.freeze + end; + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_match(/opt_hash_freeze/, insn) + assert_no_match(/duphash/, insn) + assert_no_match(/send/, insn) + assert_predicate([1].freeze, :frozen?) + assert_in_out_err([], <<~RUBY, [":ok"]) + class Hash + prepend Module.new { + def freeze + :ok + end + } + end + p({a:1}.freeze) + RUBY + end + + def test_peephole_hash_freeze_empty + code = "#{<<~'begin;'}\n#{<<~'end;'}" + begin; + {}.freeze + end; + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_match(/opt_hash_freeze/, insn) + assert_no_match(/duphash/, insn) + assert_no_match(/send/, insn) + assert_predicate([].freeze, :frozen?) + assert_in_out_err([], <<~RUBY, [":ok"]) + class Hash + prepend Module.new { + def freeze + :ok + end + } + end + p({}.freeze) + RUBY + end + def test_branch_condition_backquote bug = '[ruby-core:80740] [Bug #13444] redefined backquote should be called' class << self @@ -773,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 @@ -907,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}" @@ -921,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}" @@ -933,4 +1112,167 @@ class TestRubyOptimization < Test::Unit::TestCase def o.to_s; 1; end assert_match %r{\A#<Object:0x[0-9a-f]+>\z}, "#{o}" end + + def test_opt_duparray_send_include_p + [ + 'x = :b; [:a, :b].include?(x)', + '@c = :b; [:a, :b].include?(@c)', + '@c = "b"; %i[a b].include?(@c.to_sym)', + '[:a, :b].include?(self) == false', + ].each do |code| + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_match(/opt_duparray_send/, insn) + assert_no_match(/\bduparray\b/, insn) + assert_equal(true, eval(code)) + end + + x, y = :b, :c + assert_equal(true, [:a, :b].include?(x)) + assert_equal(false, [:a, :b].include?(y)) + + assert_in_out_err([], <<~RUBY, ["1,2", "3,3", "1,2", "4,4"]) + class Array + prepend(Module.new do + def include?(i) + puts self.join(",") + # Modify self to prove that we are operating on a copy. + map! { i } + puts self.join(",") + end + end) + end + def x(i) + [1, 2].include?(i) + end + x(3) + x(4) + RUBY + + # Ensure raises happen correctly. + assert_in_out_err([], <<~RUBY, ["will raise", "int 1 not 3"]) + class Integer + undef_method :== + def == x + raise "int \#{self} not \#{x}" + end + end + x = 3 + puts "will raise" + begin + p [1, 2].include?(x) + rescue + puts $! + end + RUBY + end + + def test_opt_newarray_send_include_p + [ + 'b = :b; [:a, b].include?(:b)', + # Use Object.new to ensure that we get newarray rather than duparray. + 'value = 1; [Object.new, true, "true", 1].include?(value)', + 'value = 1; [Object.new, "1"].include?(value.to_s)', + '[Object.new, "1"].include?(self) == false', + ].each do |code| + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_match(/opt_newarray_send/, insn) + assert_no_match(/\bnewarray\b/, insn) + assert_equal(true, eval(code)) + end + + x, y = :b, :c + assert_equal(true, [:a, x].include?(x)) + assert_equal(false, [:a, x].include?(y)) + + assert_in_out_err([], <<~RUBY, ["1,3", "3,3", "1,4", "4,4"]) + class Array + prepend(Module.new do + def include?(i) + puts self.join(",") + # Modify self to prove that we are operating on a copy. + map! { i } + puts self.join(",") + end + end) + end + def x(i) + [1, i].include?(i) + end + x(3) + x(4) + RUBY + + # Ensure raises happen correctly. + assert_in_out_err([], <<~RUBY, ["will raise", "int 1 not 3"]) + class Integer + undef_method :== + def == x + raise "int \#{self} not \#{x}" + end + end + x = 3 + puts "will raise" + begin + p [1, x].include?(x) + rescue + puts $! + end + RUBY + end + + def test_opt_new_with_safe_navigation + payload = nil + assert_nil payload&.new + end + + def test_opt_new + pos_initialize = " + def initialize a, b + @a = a + @b = b + end + " + kw_initialize = " + def initialize a:, b: + @a = a + @b = b + end + " + kw_hash_initialize = " + def initialize a, **kw + @a = a + @b = kw[:b] + end + " + pos_prelude = "class OptNewFoo; #{pos_initialize}; end;" + kw_prelude = "class OptNewFoo; #{kw_initialize}; end;" + kw_hash_prelude = "class OptNewFoo; #{kw_hash_initialize}; end;" + [ + "#{pos_prelude} OptNewFoo.new 1, 2", + "#{pos_prelude} a = 1; b = 2; OptNewFoo.new a, b", + "#{pos_prelude} def optnew_foo(a, b) = OptNewFoo.new(a, b); optnew_foo 1, 2", + "#{pos_prelude} def optnew_foo(*a) = OptNewFoo.new(*a); optnew_foo 1, 2", + "#{pos_prelude} def optnew_foo(...) = OptNewFoo.new(...); optnew_foo 1, 2", + "#{kw_prelude} def optnew_foo(**a) = OptNewFoo.new(**a); optnew_foo a: 1, b: 2", + "#{kw_hash_prelude} def optnew_foo(*a, **b) = OptNewFoo.new(*a, **b); optnew_foo 1, b: 2", + ].each do |code| + iseq = RubyVM::InstructionSequence.compile(code) + 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(&)', + 'def optnew_foo(a, ...) = OptNewFoo.new(a, ...)', + ].each do |code| + iseq = RubyVM::InstructionSequence.compile(code) + insn = iseq.disasm + assert_no_match(/opt_new/, insn) + end + end end diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index 27573ef457..ca089f09c3 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -1,8 +1,13 @@ # coding: US-ASCII # frozen_string_literal: false require 'test/unit' +require 'rbconfig' +require 'rbconfig/sizeof' class TestPack < Test::Unit::TestCase + # Note: the size of intptr_t and uintptr_t should be equal. + J_SIZE = RbConfig::SIZEOF['uintptr_t'] + def test_pack format = "c2x5CCxsdils_l_a6"; # Need the expression in here to force ary[5] to be numeric. This avoids @@ -93,11 +98,11 @@ class TestPack < Test::Unit::TestCase assert_equal("\x01\x02\x03\x04", [0x01020304].pack("L"+mod)) assert_equal("\x01\x02\x03\x04\x05\x06\x07\x08", [0x0102030405060708].pack("q"+mod)) assert_equal("\x01\x02\x03\x04\x05\x06\x07\x08", [0x0102030405060708].pack("Q"+mod)) - psize = [nil].pack('p').bytesize - if psize == 4 + case J_SIZE + when 4 assert_equal("\x01\x02\x03\x04", [0x01020304].pack("j"+mod)) assert_equal("\x01\x02\x03\x04", [0x01020304].pack("J"+mod)) - elsif psize == 8 + when 8 assert_equal("\x01\x02\x03\x04\x05\x06\x07\x08", [0x0102030405060708].pack("j"+mod)) assert_equal("\x01\x02\x03\x04\x05\x06\x07\x08", [0x0102030405060708].pack("J"+mod)) end @@ -109,10 +114,11 @@ class TestPack < Test::Unit::TestCase assert_match(/\A\x00*\x01\x02\x03\x04\z/, [0x01020304].pack("I!"+mod)) assert_match(/\A\x00*\x01\x02\x03\x04\z/, [0x01020304].pack("l!"+mod)) assert_match(/\A\x00*\x01\x02\x03\x04\z/, [0x01020304].pack("L!"+mod)) - if psize == 4 + case J_SIZE + when 4 assert_match(/\A\x00*\x01\x02\x03\x04\z/, [0x01020304].pack("j!"+mod)) assert_match(/\A\x00*\x01\x02\x03\x04\z/, [0x01020304].pack("J!"+mod)) - elsif psize == 8 + when 8 assert_match(/\A\x00*\x01\x02\x03\x04\x05\x06\x07\x08\z/, [0x0102030405060708].pack("j!"+mod)) assert_match(/\A\x00*\x01\x02\x03\x04\x05\x06\x07\x08\z/, [0x0102030405060708].pack("J!"+mod)) end @@ -141,11 +147,11 @@ class TestPack < Test::Unit::TestCase assert_equal("\x04\x03\x02\x01", [0x01020304].pack("L"+mod)) assert_equal("\x08\x07\x06\x05\x04\x03\x02\x01", [0x0102030405060708].pack("q"+mod)) assert_equal("\x08\x07\x06\x05\x04\x03\x02\x01", [0x0102030405060708].pack("Q"+mod)) - psize = [nil].pack('p').bytesize - if psize == 4 + case J_SIZE + when 4 assert_equal("\x04\x03\x02\x01", [0x01020304].pack("j"+mod)) assert_equal("\x04\x03\x02\x01", [0x01020304].pack("J"+mod)) - elsif psize == 8 + when 8 assert_equal("\x08\x07\x06\x05\x04\x03\x02\x01", [0x0102030405060708].pack("j"+mod)) assert_equal("\x08\x07\x06\x05\x04\x03\x02\x01", [0x0102030405060708].pack("J"+mod)) end @@ -157,10 +163,11 @@ class TestPack < Test::Unit::TestCase assert_match(/\A\x04\x03\x02\x01\x00*\z/, [0x01020304].pack("I!"+mod)) assert_match(/\A\x04\x03\x02\x01\x00*\z/, [0x01020304].pack("l!"+mod)) assert_match(/\A\x04\x03\x02\x01\x00*\z/, [0x01020304].pack("L!"+mod)) - if psize == 4 + case J_SIZE + when 4 assert_match(/\A\x04\x03\x02\x01\x00*\z/, [0x01020304].pack("j!"+mod)) assert_match(/\A\x04\x03\x02\x01\x00*\z/, [0x01020304].pack("J!"+mod)) - elsif psize == 8 + when 8 assert_match(/\A\x08\x07\x06\x05\x04\x03\x02\x01\x00*\z/, [0x0102030405060708].pack("j!"+mod)) assert_match(/\A\x08\x07\x06\x05\x04\x03\x02\x01\x00*\z/, [0x0102030405060708].pack("J!"+mod)) end @@ -196,8 +203,8 @@ class TestPack < Test::Unit::TestCase end def test_integer_endian_explicit - _integer_big_endian('>') - _integer_little_endian('<') + _integer_big_endian('>') + _integer_little_endian('<') end def test_pack_U @@ -442,7 +449,6 @@ class TestPack < Test::Unit::TestCase assert_operator(4, :<=, [1].pack("L!").bytesize) end - require 'rbconfig' def test_pack_unpack_qQ s1 = [578437695752307201, -506097522914230529].pack("q*") s2 = [578437695752307201, 17940646550795321087].pack("Q*") @@ -465,10 +471,8 @@ class TestPack < Test::Unit::TestCase end if RbConfig::CONFIG['HAVE_LONG_LONG'] def test_pack_unpack_jJ - # Note: we assume that the size of intptr_t and uintptr_t equals to the size - # of real pointer. - psize = [nil].pack("p").bytesize - if psize == 4 + case J_SIZE + when 4 s1 = [67305985, -50462977].pack("j*") s2 = [67305985, 4244504319].pack("J*") assert_equal(s1, s2) @@ -482,7 +486,7 @@ class TestPack < Test::Unit::TestCase assert_equal(4, [1].pack("j").bytesize) assert_equal(4, [1].pack("J").bytesize) - elsif psize == 8 + when 8 s1 = [578437695752307201, -506097522914230529].pack("j*") s2 = [578437695752307201, 17940646550795321087].pack("J*") assert_equal(s1, s2) @@ -891,4 +895,45 @@ EXPECTED } assert_equal [nil], "a".unpack("C", offset: 1) end + + def test_monkey_pack + assert_separately([], <<-'end;') + $-w = false + class Array + alias :old_pack :pack + def pack _; "oh no"; end + end + + v = [2 ** 15].pack('n') + + class Array + alias :pack :old_pack + end + + assert_equal "oh no", v + end; + end + + def test_monkey_pack_buffer + assert_separately([], <<-'end;') + $-w = false + class Array + alias :old_pack :pack + def pack _, buffer:; buffer << " no"; end + end + + def test + b = +"oh" + [2 ** 15].pack('n', buffer: b) + end + + v = test + + class Array + alias :pack :old_pack + end + + assert_equal "oh no", v + end; + end end diff --git a/test/ruby/test_parse.rb b/test/ruby/test_parse.rb index 49b25fcaf0..def41d6017 100644 --- a/test/ruby/test_parse.rb +++ b/test/ruby/test_parse.rb @@ -2,6 +2,7 @@ # frozen_string_literal: false require 'test/unit' require 'stringio' +require_relative '../lib/parser_support' class TestParse < Test::Unit::TestCase def setup @@ -18,7 +19,7 @@ class TestParse < Test::Unit::TestCase end def test_else_without_rescue - assert_syntax_error(<<-END, %r":#{__LINE__+2}: else without rescue"o, [__FILE__, __LINE__+1]) + assert_syntax_error(<<-END, %r"(:#{__LINE__+2}:|#{__LINE__+2} \|.+?\n.+?\^~.+?;) else without rescue"o, [__FILE__, __LINE__+1]) begin else 42 @@ -185,6 +186,15 @@ class TestParse < Test::Unit::TestCase end; end + c = Class.new + c.freeze + assert_valid_syntax("#{<<~"begin;"}\n#{<<~'end;'}") do + begin; + c::FOO &= p 1 + ::FOO &= p 1 + end; + end + assert_syntax_error("#{<<~"begin;"}\n#{<<~'end;'}", /Can't set variable/) do begin; $1 &= 1 @@ -342,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("``") @@ -378,10 +403,10 @@ class TestParse < Test::Unit::TestCase def assert_disallowed_variable(type, noname, invalid) noname.each do |name| - assert_syntax_error("proc{a = #{name} }", "`#{noname[0]}' without identifiers is not allowed as #{type} variable name") + assert_syntax_error("proc{a = #{name} }", "'#{noname[0]}' without identifiers is not allowed as #{type} variable name") end invalid.each do |name| - assert_syntax_error("proc {a = #{name} }", "`#{name}' is not allowed as #{type} variable name") + assert_syntax_error("proc {a = #{name} }", "'#{name}' is not allowed as #{type} variable name") end end @@ -453,10 +478,52 @@ class TestParse < Test::Unit::TestCase end def test_define_singleton_error - assert_syntax_error("#{<<~"begin;"}\n#{<<~'end;'}", /singleton method for literals/) do - begin; - def ("foo").foo; end - end; + msg = /singleton method for literals/ + assert_parse_error(%q[def ("foo").foo; end], msg) + assert_parse_error(%q[def (1).foo; end], msg) + assert_parse_error(%q[def ((1;1)).foo; end], msg) + assert_parse_error(%q[def ((;1)).foo; end], msg) + assert_parse_error(%q[def ((1+1;1)).foo; end], msg) + assert_parse_error(%q[def ((%s();1)).foo; end], msg) + assert_parse_error(%q[def ((%w();1)).foo; end], msg) + assert_parse_error(%q[def ("#{42}").foo; end], msg) + assert_parse_error(%q[def (:"#{42}").foo; end], msg) + assert_parse_error(%q[def ([]).foo; end], msg) + assert_parse_error(%q[def ([1]).foo; end], msg) + assert_parse_error(%q[def (__FILE__).foo; end], msg) + assert_parse_error(%q[def (__LINE__).foo; end], msg) + assert_parse_error(%q[def (__ENCODING__).foo; end], msg) + assert_parse_error(%q[def __FILE__.foo; end], msg) + assert_parse_error(%q[def __LINE__.foo; end], msg) + assert_parse_error(%q[def __ENCODING__.foo; end], msg) + end + + def test_flip_flop + all_assertions_foreach(nil, + ['(cond1..cond2)', true], + ['((cond1..cond2))', true], + + # '(;;;cond1..cond2)', # don't care + + '(1; cond1..cond2)', + '(%s(); cond1..cond2)', + '(%w(); cond1..cond2)', + '(1; (2; (3; 4; cond1..cond2)))', + '(1+1; cond1..cond2)', + ) do |code, pass| + code = code.sub("cond1", "n==4").sub("cond2", "n==5") + if pass + assert_equal([4,5], eval("(1..9).select {|n| true if #{code}}")) + else + assert_raise_with_message(ArgumentError, /bad value for range/, code) { + verbose_bak, $VERBOSE = $VERBOSE, nil # disable "warning: possibly useless use of a literal in void context" + begin + eval("[4].each {|n| true if #{code}}") + ensure + $VERBOSE = verbose_bak + end + } + end end end @@ -464,6 +531,10 @@ class TestParse < Test::Unit::TestCase t = Object.new a = [] blk = proc {|x| a << x } + + # Prevent an "assigned but unused variable" warning + _ = blk + def t.[](_) yield(:aref) nil @@ -473,16 +544,16 @@ class TestParse < Test::Unit::TestCase end def t.dummy(_) end - eval <<-END, nil, __FILE__, __LINE__+1 + + assert_syntax_error("#{<<~"begin;"}\n#{<<~'end;'}", /block arg given in index assignment/) + begin; t[42, &blk] ||= 42 - END - assert_equal([:aref, :aset], a) - a.clear - eval <<-END, nil, __FILE__, __LINE__+1 - t[42, &blk] ||= t.dummy 42 # command_asgn test - END - assert_equal([:aref, :aset], a) - blk + end; + + assert_syntax_error("#{<<~"begin;"}\n#{<<~'end;'}", /block arg given in index assignment/) + begin; + t[42, &blk] ||= t.dummy 42 # command_asgn test + end; end def test_backquote @@ -493,7 +564,7 @@ class TestParse < Test::Unit::TestCase def t.`(x); "foo" + x + "bar"; end END end - a = b = nil + a = b = c = nil assert_nothing_raised do eval <<-END, nil, __FILE__, __LINE__+1 a = t.` "zzz" @@ -501,10 +572,12 @@ class TestParse < Test::Unit::TestCase END t.instance_eval <<-END, __FILE__, __LINE__+1 b = `zzz` + c = %x(ccc) END end assert_equal("foozzzbar", a) assert_equal("foozzzbar", b) + assert_equal("foocccbar", c) end def test_carrige_return @@ -515,34 +588,42 @@ class TestParse < Test::Unit::TestCase mesg = 'from the backslash through the invalid char' e = assert_syntax_error('"\xg1"', /hex escape/) - assert_equal(' ^~'"\n", e.message.lines.last, mesg) + assert_match(/(^|\| ) \^~(?!~)/, e.message.lines.last, mesg) e = assert_syntax_error('"\u{1234"', 'unterminated Unicode escape') - assert_equal(' ^'"\n", e.message.lines.last, mesg) + assert_match(/(^|\| ) \^(?!~)/, e.message.lines.last, mesg) e = assert_syntax_error('"\u{xxxx}"', 'invalid Unicode escape') - assert_equal(' ^'"\n", e.message.lines.last, mesg) + assert_match(/(^|\| ) \^(?!~)/, e.message.lines.last, mesg) e = assert_syntax_error('"\u{xxxx', 'Unicode escape') - assert_pattern_list([ - /.*: invalid Unicode escape\n.*\n/, - / \^/, - /\n/, - /.*: unterminated Unicode escape\n.*\n/, - / \^/, - /\n/, - /.*: unterminated string.*\n.*\n/, - / \^\n/, - ], e.message) + if e.message.lines.first == "#{__FILE__}:#{__LINE__ - 1}: syntax errors found\n" + assert_pattern_list([ + /\s+\| \^ unterminated string;.+\n/, + /\s+\| \^ unterminated Unicode escape\n/, + /\s+\| \^ invalid Unicode escape sequence\n/, + ], e.message.lines[2..-1].join) + else + assert_pattern_list([ + /.*: invalid Unicode escape\n.*\n/, + / \^/, + /\n/, + /.*: unterminated Unicode escape\n.*\n/, + / \^/, + /\n/, + /.*: unterminated string.*\n.*\n/, + / \^\n/, + ], e.message) + end e = assert_syntax_error('"\M1"', /escape character syntax/) - assert_equal(' ^~~'"\n", e.message.lines.last, mesg) + assert_match(/(^|\| ) \^~~(?!~)/, e.message.lines.last, mesg) e = assert_syntax_error('"\C1"', /escape character syntax/) - assert_equal(' ^~~'"\n", e.message.lines.last, mesg) + assert_match(/(^|\| ) \^~~(?!~)/, e.message.lines.last, mesg) src = '"\xD0\u{90'"\n""000000000000000000000000" - assert_syntax_error(src, /:#{__LINE__}: unterminated/o) + assert_syntax_error(src, /(:#{__LINE__}:|> #{__LINE__} \|.+) unterminated/om) assert_syntax_error('"\u{100000000}"', /invalid Unicode escape/) assert_equal("", eval('"\u{}"')) @@ -565,19 +646,23 @@ class TestParse < Test::Unit::TestCase assert_syntax_error("\"\\C-\\M-\x01\"", 'Invalid escape character syntax') e = assert_syntax_error('"\c\u0000"', 'Invalid escape character syntax') - assert_equal(' ^~~~'"\n", e.message.lines.last) + assert_match(/(^|\| ) \^~~~(?!~)/, e.message.lines.last) e = assert_syntax_error('"\c\U0000"', 'Invalid escape character syntax') - assert_equal(' ^~~~'"\n", e.message.lines.last) + assert_match(/(^|\| ) \^~~~(?!~)/, e.message.lines.last) e = assert_syntax_error('"\C-\u0000"', 'Invalid escape character syntax') - assert_equal(' ^~~~~'"\n", e.message.lines.last) + assert_match(/(^|\| ) \^~~~~(?!~)/, e.message.lines.last) e = assert_syntax_error('"\C-\U0000"', 'Invalid escape character syntax') - assert_equal(' ^~~~~'"\n", e.message.lines.last) + assert_match(/(^|\| ) \^~~~~(?!~)/, e.message.lines.last) e = assert_syntax_error('"\M-\u0000"', 'Invalid escape character syntax') - assert_equal(' ^~~~~'"\n", e.message.lines.last) + assert_match(/(^|\| ) \^~~~~(?!~)/, e.message.lines.last) e = assert_syntax_error('"\M-\U0000"', 'Invalid escape character syntax') - assert_equal(' ^~~~~'"\n", e.message.lines.last) + assert_match(/(^|\| ) \^~~~~(?!~)/, e.message.lines.last) + + e = assert_syntax_error(%["\\C-\u3042"], 'Invalid escape character syntax') + assert_match(/(^|\|\s)\s \^(?# \\ ) ~(?# C ) ~(?# - ) ~+(?# U+3042 )($|\s)/x, e.message.lines.last) + assert_not_include(e.message, "invalid multibyte char") end def test_question @@ -593,6 +678,8 @@ class TestParse < Test::Unit::TestCase assert_equal("\u{1234}", eval('?\u{1234}')) assert_equal("\u{1234}", eval('?\u1234')) assert_syntax_error('?\u{41 42}', 'Multiple codepoints at single character literal') + assert_syntax_error("?and", /unexpected '\?'/) + assert_syntax_error("?\u1234and", /unexpected '\?'/) e = assert_syntax_error('"#{?\u123}"', 'invalid Unicode escape') assert_not_match(/end-of-input/, e.message) @@ -608,6 +695,8 @@ class TestParse < Test::Unit::TestCase assert_syntax_error("?\\M-\x01", 'Invalid escape character syntax') assert_syntax_error("?\\M-\\C-\x01", 'Invalid escape character syntax') assert_syntax_error("?\\C-\\M-\x01", 'Invalid escape character syntax') + + assert_equal("\xff", eval("# encoding: ascii-8bit\n""?\\\xFF")) end def test_percent @@ -641,6 +730,7 @@ class TestParse < Test::Unit::TestCase assert_syntax_error(':@@1', /is not allowed/) assert_syntax_error(':@', /is not allowed/) assert_syntax_error(':@1', /is not allowed/) + assert_syntax_error(':$01234', /is not allowed/) end def test_parse_string @@ -726,6 +816,54 @@ x = __ENCODING__ END end assert_equal(__ENCODING__, x) + + assert_raise(ArgumentError) do + EnvUtil.with_default_external(Encoding::US_ASCII) {eval <<-END, nil, __FILE__, __LINE__+1} +# coding = external +x = __ENCODING__ + END + end + + assert_raise(ArgumentError) do + EnvUtil.with_default_internal(Encoding::US_ASCII) {eval <<-END, nil, __FILE__, __LINE__+1} +# coding = internal +x = __ENCODING__ + END + end + + assert_raise(ArgumentError) do + eval <<-END, nil, __FILE__, __LINE__+1 +# coding = filesystem +x = __ENCODING__ + END + end + + assert_raise(ArgumentError) do + eval <<-END, nil, __FILE__, __LINE__+1 +# coding = locale +x = __ENCODING__ + END + end + + e = assert_raise(ArgumentError) do + eval <<-END, nil, __FILE__, __LINE__+1 +# coding: foo + END + end + + message = e.message.gsub(/\033\[.*?m/, "") + assert_include(message, "# coding: foo\n") + assert_include(message, " ^") + + e = assert_raise(ArgumentError) do + eval <<-END, nil, __FILE__, __LINE__+1 +# coding = foo + END + end + + message = e.message.gsub(/\033\[.*?m/, "") + assert_include(message, "# coding = foo\n") + assert_include(message, " ^") end def test_utf8_bom @@ -769,7 +907,7 @@ x = __ENCODING__ def test_float assert_predicate(assert_warning(/out of range/) {eval("1e10000")}, :infinite?) - assert_syntax_error('1_E', /trailing `_'/) + assert_syntax_error('1_E', /trailing '_'/) assert_syntax_error('1E1E1', /unexpected constant/) end @@ -780,7 +918,8 @@ x = __ENCODING__ $test_parse_foobarbazqux = nil assert_equal(nil, $&) assert_equal(nil, eval('alias $& $preserve_last_match')) - assert_syntax_error('a = $#', /as a global variable name\na = \$\#\n \^~$/) + assert_syntax_error('a = $#', /as a global variable name/) + assert_syntax_error('a = $#', /a = \$\#\n(^|.+?\| ) \^~(?!~)/) end def test_invalid_instance_variable @@ -797,7 +936,7 @@ x = __ENCODING__ def test_invalid_char bug10117 = '[ruby-core:64243] [Bug #10117]' - invalid_char = /Invalid char `\\x01'/ + invalid_char = /Invalid char '\\x01'/ x = 1 assert_in_out_err(%W"-e \x01x", "", [], invalid_char, bug10117) assert_syntax_error("\x01x", invalid_char, bug10117) @@ -831,24 +970,9 @@ x = __ENCODING__ assert_syntax_error("$& = 1", /Can't set variable/) end - def test_arg_concat - o = Object.new - class << o; self; end.instance_eval do - define_method(:[]=) {|*r, &b| b.call(r) } - end - r = nil - assert_nothing_raised do - eval <<-END, nil, __FILE__, __LINE__+1 - o[&proc{|x| r = x }] = 1 - END - end - assert_equal([1], r) - end - def test_void_expr_stmts_value x = 1 useless_use = /useless use/ - unused = /unused/ assert_nil assert_warning(useless_use) {eval("x; nil")} assert_nil assert_warning(useless_use) {eval("1+1; nil")} assert_nil assert_warning('') {eval("1.+(1); nil")} @@ -856,24 +980,27 @@ x = __ENCODING__ assert_nil assert_warning(useless_use) {eval("::TestParse; nil")} assert_nil assert_warning(useless_use) {eval("x..x; nil")} assert_nil assert_warning(useless_use) {eval("x...x; nil")} - assert_nil assert_warning(unused) {eval("self; nil")} - assert_nil assert_warning(unused) {eval("nil; nil")} - assert_nil assert_warning(unused) {eval("true; nil")} - assert_nil assert_warning(unused) {eval("false; nil")} + assert_nil assert_warning(useless_use) {eval("self; nil")} + assert_nil assert_warning(useless_use) {eval("nil; nil")} + assert_nil assert_warning(useless_use) {eval("true; nil")} + assert_nil assert_warning(useless_use) {eval("false; nil")} assert_nil assert_warning(useless_use) {eval("defined?(1); nil")} + assert_nil assert_warning(useless_use) {eval("begin; ensure; x; end")} assert_equal 1, x assert_syntax_error("1; next; 2", /Invalid next/) end def test_assign_in_conditional - assert_warning(/`= literal' in conditional/) do + # multiple assignment + assert_warning(/'= literal' in conditional/) do eval <<-END, nil, __FILE__, __LINE__+1 (x, y = 1, 2) ? 1 : 2 END end - assert_warning(/`= literal' in conditional/) do + # instance variable assignment + assert_warning(/'= literal' in conditional/) do eval <<-END, nil, __FILE__, __LINE__+1 if @x = true 1 @@ -882,6 +1009,71 @@ x = __ENCODING__ end END end + + # local variable assignment + assert_warning(/'= literal' in conditional/) do + eval <<-END, nil, __FILE__, __LINE__+1 + def m + if x = true + 1 + else + 2 + end + end + END + end + + # global variable assignment + assert_separately([], <<-RUBY) + assert_warning(/'= literal' in conditional/) do + eval <<-END, nil, __FILE__, __LINE__+1 + if $x = true + 1 + else + 2 + end + END + end + RUBY + + # dynamic variable assignment + assert_warning(/'= literal' in conditional/) do + eval <<-END, nil, __FILE__, __LINE__+1 + y = 1 + + 1.times do + if y = true + 1 + else + 2 + end + end + END + end + + # class variable assignment + assert_warning(/'= literal' in conditional/) do + eval <<-END, nil, __FILE__, __LINE__+1 + c = Class.new + class << c + if @@a = 1 + end + end + END + end + + # constant declaration + assert_separately([], <<-RUBY) + assert_warning(/'= literal' in conditional/) do + eval <<-END, nil, __FILE__, __LINE__+1 + if Const = true + 1 + else + 2 + end + END + end + RUBY end def test_literal_in_conditional @@ -958,6 +1150,20 @@ x = __ENCODING__ assert_warning('') {o.instance_eval("def marg2((a)); nil; end")} end + def test_parsing_begin_statement_inside_method_definition + assert_equal :bug_20234, eval("def (begin;end).bug_20234; end") + NilClass.remove_method(:bug_20234) + assert_equal :bug_20234, eval("def (begin;rescue;end).bug_20234; end") + NilClass.remove_method(:bug_20234) + assert_equal :bug_20234, eval("def (begin;ensure;end).bug_20234; end") + NilClass.remove_method(:bug_20234) + assert_equal :bug_20234, eval("def (begin;rescue;else;end).bug_20234; end") + NilClass.remove_method(:bug_20234) + + assert_raise(SyntaxError) { eval("def (begin;else;end).bug_20234; end") } + assert_raise(SyntaxError) { eval("def (begin;ensure;else;end).bug_20234; end") } + end + def test_named_capture_conflict a = 1 assert_warning('') {eval("a = 1; /(?<a>)/ =~ ''")} @@ -965,6 +1171,30 @@ x = __ENCODING__ assert_warning('') {eval("#{a} = 1; /(?<#{a}>)/ =~ ''")} end + def test_named_capture_in_block + all_assertions_foreach(nil, + '(/(?<a>.*)/)', + '(;/(?<a>.*)/)', + '(%s();/(?<a>.*)/)', + '(%w();/(?<a>.*)/)', + '(1; (2; 3; (4; /(?<a>.*)/)))', + '(1+1; /(?<a>.*)/)', + '/#{""}(?<a>.*)/', + ) do |code, pass| + token = Random.bytes(4).unpack1("H*") + if pass + assert_equal(token, eval("#{code} =~ #{token.dump}; a")) + else + verbose_bak, $VERBOSE = $VERBOSE, nil # disable "warning: possibly useless use of a literal in void context" + begin + assert_nil(eval("#{code} =~ #{token.dump}; defined?(a)"), code) + ensure + $VERBOSE = verbose_bak + end + end + end + end + def test_rescue_in_command_assignment bug = '[ruby-core:75621] [Bug #12402]' all_assertions(bug) do |a| @@ -1052,14 +1282,30 @@ x = __ENCODING__ assert_syntax_error(" 0b\n", /\^/) end + def test_unclosed_unicode_escape_at_eol_bug_19750 + assert_separately([], "#{<<-"begin;"}\n#{<<~'end;'}") + begin; + assert_syntax_error("/\\u", /too short escape sequence/) + assert_syntax_error("/\\u{", /unterminated regexp meets end of file/) + assert_syntax_error("/\\u{\\n", /invalid Unicode list/) + assert_syntax_error("/a#\\u{\\n/", /invalid Unicode list/) + re = eval("/a#\\u{\n$/x") + assert_match(re, 'a') + assert_not_match(re, 'a#') + re = eval("/a#\\u\n$/x") + assert_match(re, 'a') + assert_not_match(re, 'a#') + end; + end + def test_error_def_in_argument assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}") begin; assert_syntax_error("def f r:def d; def f 0end", /unexpected/) end; - assert_syntax_error("def\nf(000)end", /^ \^~~/) - assert_syntax_error("def\nf(&0)end", /^ \^/) + assert_syntax_error("def\nf(000)end", /(^|\| ) \^~~/) + assert_syntax_error("def\nf(&0)end", /(^|\| ) \^/) end def test_method_location_in_rescue @@ -1095,46 +1341,61 @@ x = __ENCODING__ end; end - def test_heredoc_interpolation - var = 1 + def test_heredoc_interpolation + var = 1 - v1 = <<~HEREDOC - something - #{"/#{var}"} - HEREDOC + v1 = <<~HEREDOC + something + #{"/#{var}"} + HEREDOC - v2 = <<~HEREDOC - something - #{_other = "/#{var}"} - HEREDOC + v2 = <<~HEREDOC + something + #{_other = "/#{var}"} + HEREDOC - v3 = <<~HEREDOC - something - #{("/#{var}")} - HEREDOC + v3 = <<~HEREDOC + something + #{("/#{var}")} + HEREDOC - assert_equal "something\n/1\n", v1 - assert_equal "something\n/1\n", v2 - assert_equal "something\n/1\n", v3 - assert_equal v1, v2 - assert_equal v2, v3 - assert_equal v1, v3 - end + assert_equal "something\n/1\n", v1 + assert_equal "something\n/1\n", v2 + assert_equal "something\n/1\n", v3 + assert_equal v1, v2 + assert_equal v2, v3 + assert_equal v1, v3 + end + + def test_heredoc_unterminated_interpolation + code = <<~'HEREDOC' + <<A+1 + #{ + HEREDOC + + assert_syntax_error(code, /can't find string "A"/) + end def test_unexpected_token_error assert_syntax_error('"x"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', /unexpected/) end def test_unexpected_token_after_numeric - assert_syntax_error('0000xyz', /^ \^~~\Z/) - assert_syntax_error('1.2i1.1', /^ \^~~\Z/) - assert_syntax_error('1.2.3', /^ \^~\Z/) + assert_syntax_error('0000xyz', /(^|\| ) \^~~(?!~)/) + assert_syntax_error('1.2i1.1', /(^|\| ) \^~~(?!~)/) + assert_syntax_error('1.2.3', /(^|\| ) \^~(?!~)/) + assert_syntax_error('1.', /unexpected end-of-input/) + assert_syntax_error('1e', /expecting end-of-input/) end def test_truncated_source_line - e = assert_syntax_error("'0123456789012345678901234567890123456789' abcdefghijklmnopqrstuvwxyz0123456789 0123456789012345678901234567890123456789", + lineno = __LINE__ + 1 + e = assert_syntax_error("'0123456789012345678901234567890123456789' abcdefghijklmnopqrstuvwxyz0123456789 123456789012345678901234567890123456789", /unexpected local variable or method/) + line = e.message.lines[1] + line.delete_prefix!("> #{lineno} | ") if line.start_with?(">") + assert_operator(line, :start_with?, "...") assert_operator(line, :end_with?, "...\n") end @@ -1178,11 +1439,11 @@ x = __ENCODING__ end def test_unexpected_eof - assert_syntax_error('unless', /^ \^\Z/) + assert_syntax_error('unless', /(^|\| ) \^(?!~)/) end def test_location_of_invalid_token - assert_syntax_error('class xxx end', /^ \^~~\Z/) + assert_syntax_error('class xxx end', /(^|\| ) \^~~(?!~)/) end def test_whitespace_warning @@ -1248,10 +1509,23 @@ x = __ENCODING__ def test_void_value_in_rhs w = "void value expression" - ["x = return 1", "x = return, 1", "x = 1, return", "x, y = return"].each do |code| + [ + "x = return 1", "x = return, 1", "x = 1, return", "x, y = return", + "x = begin return ensure end", + "x = begin ensure return end", + "x = begin return ensure return end", + "x = begin return; rescue; return end", + "x = begin return; rescue; return; else return end", + ].each do |code| ex = assert_syntax_error(code, w) assert_equal(1, ex.message.scan(w).size, ->{"same #{w.inspect} warning should be just once\n#{w.message}"}) end + [ + "x = begin return; rescue; end", + "x = begin return; rescue; return; else end", + ].each do |code| + assert_valid_syntax(code) + end end def eval_separately(code) @@ -1285,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 @@ -1314,7 +1588,40 @@ 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]' + a, b = eval_separately(<<~'end;') + # shareable_constant_value: literal + foo = 1 + bar = 2 + A = { foo => bar } + B = [foo, bar] + [A, B] + end; + + assert_ractor_shareable(a) + assert_ractor_shareable(b) + assert_equal([1], a.keys, bug_20339) + assert_equal([2], a.values, bug_20339) + assert_equal(1, b[0], bug_20341) + assert_equal(2, b[1], bug_20341) + end + + def test_shareable_constant_value_literal_const_refs + a = eval_separately("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # shareable_constant_value: literal + # [Bug #20668] + SOME_CONST = { + 'Object' => Object, + 'String' => String, + 'Array' => Array, + } + SOME_CONST + end; + assert_ractor_shareable(a) end def test_shareable_constant_value_nested @@ -1335,13 +1642,89 @@ x = __ENCODING__ assert_ractor_shareable(a[0]) end + def test_shareable_constant_value_hash_with_keyword_splat + a, b = eval_separately("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # shareable_constant_value: experimental_everything + # [Bug #20927] + x = { x: {} } + y = { y: {} } + A = { **x } + B = { x: {}, **y } + [A, B] + end; + assert_ractor_shareable(a) + assert_ractor_shareable(b) + assert_equal({ x: {}}, a) + assert_equal({ x: {}, y: {}}, b) + end + def test_shareable_constant_value_unshareable_literal - assert_raise_separately(Ractor::IsolationError, /unshareable/, + assert_raise_separately(Ractor::IsolationError, /unshareable object to C/, "#{<<~"begin;"}\n#{<<~'end;'}") begin; # shareable_constant_value: literal C = ["Not " + "shareable"] end; + + assert_raise_separately(Ractor::IsolationError, /unshareable object to B::C/, + "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # shareable_constant_value: literal + B = Class.new + B::C = ["Not " + "shareable"] + end; + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_raise_with_message(Ractor::IsolationError, /unshareable object to ::C/) do + # shareable_constant_value: literal + ::C = ["Not " + "shareable"] + end + end; + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_raise_with_message(Ractor::IsolationError, /unshareable object to ::B::C/) do + # shareable_constant_value: literal + ::B = Class.new + ::B::C = ["Not " + "shareable"] + end + end; + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_raise_with_message(Ractor::IsolationError, /unshareable object to ::C/) do + # shareable_constant_value: literal + ::C ||= ["Not " + "shareable"] + end + end; + + assert_raise_separately(Ractor::IsolationError, /unshareable object to B::C/, + "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # shareable_constant_value: literal + B = Class.new + B::C ||= ["Not " + "shareable"] + end; + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_raise_with_message(Ractor::IsolationError, /unshareable object to ::B::C/) do + # shareable_constant_value: literal + ::B = Class.new + ::B::C ||= ["Not " + "shareable"] + end + end; + + assert_raise_separately(Ractor::IsolationError, /unshareable object to ...::C/, + "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # shareable_constant_value: literal + B = Class.new + def self.expr; B; end + expr::C ||= ["Not " + "shareable"] + end; end def test_shareable_constant_value_nonliteral @@ -1370,6 +1753,15 @@ x = __ENCODING__ end; end + def test_shareable_constant_value_massign + a = eval_separately("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # shareable_constant_value: experimental_everything + A, = 1 + end; + assert_equal(1, a) + end + def test_if_after_class assert_valid_syntax('module if true; Object end::Kernel; end') assert_valid_syntax('module while true; break Object end::Kernel; end') @@ -1416,9 +1808,29 @@ x = __ENCODING__ assert_equal(expected, obj.arg) end + def test_ungettable_gvar + assert_syntax_error('$01234', /not allowed/) + assert_syntax_error('"#$01234"', /not allowed/) + end + =begin def test_past_scope_variable assert_warning(/past scope/) {catch {|tag| eval("BEGIN{throw tag}; tap {a = 1}; a")}} end =end + + def assert_parse(code) + assert_kind_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.parse(code)) + end + + def assert_parse_error(code, message) + assert_raise_with_message(SyntaxError, message) do + $VERBOSE, verbose_bak = nil, $VERBOSE + begin + RubyVM::AbstractSyntaxTree.parse(code) + ensure + $VERBOSE = verbose_bak + end + end + end end diff --git a/test/ruby/test_pattern_matching.rb b/test/ruby/test_pattern_matching.rb index 0337e5d945..96aa2a7fd6 100644 --- a/test/ruby/test_pattern_matching.rb +++ b/test/ruby/test_pattern_matching.rb @@ -9,14 +9,14 @@ class TestPatternMatching < Test::Unit::TestCase end def setup - if defined?(DidYouMean) + if defined?(DidYouMean.formatter=nil) @original_formatter = DidYouMean.formatter DidYouMean.formatter = NullFormatter.new end end def teardown - if defined?(DidYouMean) + if defined?(DidYouMean.formatter=nil) DidYouMean.formatter = @original_formatter end end @@ -109,16 +109,12 @@ class TestPatternMatching < Test::Unit::TestCase end assert_block do - # suppress "warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!" - experimental, Warning[:experimental] = Warning[:experimental], false eval(%q{ case true in a a end }) - ensure - Warning[:experimental] = experimental end assert_block do @@ -201,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 @@ -358,6 +392,14 @@ END end assert_block do + a = "abc" + case 'abc' + in /#{a}/o + true + end + end + + assert_block do case 0 in ->(i) { i == 0 } true @@ -800,6 +842,10 @@ END true end end + + assert_syntax_error(%q{ + 0 => [a, *a] + }, /duplicated variable name/) end def test_find_pattern @@ -868,6 +914,10 @@ END false end end + + assert_syntax_error(%q{ + 0 => [*a, a, b, *b] + }, /duplicated variable name/) end def test_hash_pattern @@ -1157,7 +1207,7 @@ END end end - bug18890 = assert_warning(/(?:.*:[47]: warning: unused literal ignored\n){2}/) do + bug18890 = assert_warning(/(?:.*:[47]: warning: possibly useless use of a literal in void context\n){2}/) do eval("#{<<~';;;'}") proc do |i| case i @@ -1319,7 +1369,7 @@ END end assert_block do - case {} + case C.new({}) in {} C.keys == nil end @@ -1652,7 +1702,7 @@ END raise a # suppress "unused variable: a" warning end - assert_raise_with_message(NoMatchingPatternKeyError, "{:a=>0}: key not found: :aa") do + assert_raise_with_message(NoMatchingPatternKeyError, "{a: 0}: key not found: :aa") do {a: 0} => {aa:} raise aa # suppress "unused variable: aa" warning rescue NoMatchingPatternKeyError => e @@ -1661,7 +1711,7 @@ END raise e end - assert_raise_with_message(NoMatchingPatternKeyError, "{:a=>{:b=>0}}: key not found: :bb") do + assert_raise_with_message(NoMatchingPatternKeyError, "{a: {b: 0}}: key not found: :bb") do {a: {b: 0}} => {a: {bb:}} raise bb # suppress "unused variable: bb" warning rescue NoMatchingPatternKeyError => e @@ -1670,15 +1720,15 @@ END raise e end - assert_raise_with_message(NoMatchingPatternError, "{:a=>0}: 1 === 0 does not return true") do + assert_raise_with_message(NoMatchingPatternError, "{a: 0}: 1 === 0 does not return true") do {a: 0} => {a: 1} end - assert_raise_with_message(NoMatchingPatternError, "{:a=>0}: {:a=>0} is not empty") do + assert_raise_with_message(NoMatchingPatternError, "{a: 0}: {a: 0} is not empty") do {a: 0} => {} end - assert_raise_with_message(NoMatchingPatternError, "[{:a=>0}]: rest of {:a=>0} is not empty") do + assert_raise_with_message(NoMatchingPatternError, "[{a: 0}]: rest of {a: 0} is not empty") do [{a: 0}] => [{**nil}] end end diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index 05c6e03aac..959ea87f25 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -159,13 +159,65 @@ class TestProc < Test::Unit::TestCase assert_equal(*m_nest{}, "[ruby-core:84583] Feature #14627") end - def test_hash + def test_hash_equal + # iseq backed proc + p1 = proc {} + p2 = p1.dup + + assert_equal p1.hash, p2.hash + + # ifunc backed proc + p1 = {}.to_proc + p2 = p1.dup + + assert_equal p1.hash, p2.hash + + # symbol backed proc + p1 = :hello.to_proc + p2 = :hello.to_proc + + assert_equal p1.hash, p2.hash + end + + def test_hash_uniqueness def self.capture(&block) block end - procs = Array.new(1000){capture{:foo }} - assert_operator(procs.map(&:hash).uniq.size, :>=, 500) + procs = Array.new(1000){capture{:foo }} + assert_operator(procs.map(&:hash).uniq.size, :>=, 500) + + # iseq backed proc + unique_hashes = 1000.times.map { proc {}.hash }.uniq + assert_operator(unique_hashes.size, :>=, 500) + + # ifunc backed proc + unique_hashes = 1000.times.map { {}.to_proc.hash }.uniq + assert_operator(unique_hashes.size, :>=, 500) + + # symbol backed proc + unique_hashes = 1000.times.map { |i| :"test#{i}".to_proc.hash }.uniq + assert_operator(unique_hashes.size, :>=, 500) + end + + def test_hash_does_not_change_after_compaction + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + # [Bug #20853] + [ + "proc {}", # iseq backed proc + "{}.to_proc", # ifunc backed proc + ":hello.to_proc", # symbol backed proc + ].each do |proc| + assert_separately([], <<~RUBY) + p1 = #{proc} + hash = p1.hash + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_equal(hash, p1.hash, "proc is `#{proc}`") + RUBY + end end def test_block_par @@ -207,18 +259,24 @@ class TestProc < Test::Unit::TestCase end def test_block_given_method + verbose_bak, $VERBOSE = $VERBOSE, nil m = method(:m_block_given?) assert(!m.call, "without block") assert(m.call {}, "with block") assert(!m.call, "without block second") + ensure + $VERBOSE = verbose_bak end def test_block_given_method_to_proc + verbose_bak, $VERBOSE = $VERBOSE, nil bug8341 = '[Bug #8341]' m = method(:m_block_given?).to_proc assert(!m.call, "#{bug8341} without block") assert(m.call {}, "#{bug8341} with block") assert(!m.call, "#{bug8341} without block second") + ensure + $VERBOSE = verbose_bak end def test_block_persist_between_calls @@ -289,7 +347,6 @@ class TestProc < Test::Unit::TestCase assert_equal(false, l.lambda?) assert_equal(false, l.curry.lambda?, '[ruby-core:24127]') assert_equal(false, proc(&l).lambda?) - assert_equal(false, assert_deprecated_warning {lambda(&l)}.lambda?) assert_equal(false, Proc.new(&l).lambda?) l = lambda {} assert_equal(true, l.lambda?) @@ -299,47 +356,21 @@ class TestProc < Test::Unit::TestCase assert_equal(true, Proc.new(&l).lambda?) end - def self.helper_test_warn_lamda_with_passed_block &b + def helper_test_warn_lambda_with_passed_block &b lambda(&b) end - def self.def_lambda_warning name, warn - define_method(name, proc do - prev = Warning[:deprecated] - assert_warn warn do - Warning[:deprecated] = true - yield - end - ensure - Warning[:deprecated] = prev - end) - end - - def_lambda_warning 'test_lambda_warning_normal', '' do - lambda{} - end - - def_lambda_warning 'test_lambda_warning_pass_lambda', '' do - b = lambda{} - lambda(&b) - end - - def_lambda_warning 'test_lambda_warning_pass_symbol_proc', '' do - lambda(&:to_s) - end - - def_lambda_warning 'test_lambda_warning_pass_proc', /deprecated/ do - b = proc{} - lambda(&b) - end - - def_lambda_warning 'test_lambda_warning_pass_block', /deprecated/ do - helper_test_warn_lamda_with_passed_block{} + def test_lambda_warning_pass_proc + assert_raise(ArgumentError) do + b = proc{} + lambda(&b) + end end - def_lambda_warning 'test_lambda_warning_pass_block_symbol_proc', '' do - # Symbol#to_proc returns lambda - helper_test_warn_lamda_with_passed_block(&:to_s) + def test_lambda_warning_pass_block + assert_raise(ArgumentError) do + helper_test_warn_lambda_with_passed_block{} + end end def test_curry_ski_fib @@ -398,6 +429,7 @@ class TestProc < Test::Unit::TestCase end def test_dup_clone + # iseq backed proc b = proc {|x| x + "bar" } class << b; attr_accessor :foo; end @@ -410,11 +442,50 @@ class TestProc < Test::Unit::TestCase assert_equal("foobar", bc.call("foo")) bc.foo = :foo assert_equal(:foo, bc.foo) + + # ifunc backed proc + b = {foo: "bar"}.to_proc + + bd = b.dup + assert_equal("bar", bd.call(:foo)) + + bc = b.clone + assert_equal("bar", bc.call(:foo)) + + # symbol backed proc + b = :to_s.to_proc + + bd = b.dup + assert_equal("testing", bd.call(:testing)) + + bc = b.clone + assert_equal("testing", bc.call(:testing)) end def test_dup_subclass c1 = Class.new(Proc) assert_equal c1, c1.new{}.dup.class, '[Bug #17545]' + c1 = Class.new(Proc) {def initialize_dup(*) throw :initialize_dup; end} + assert_throw(:initialize_dup) {c1.new{}.dup} + end + + def test_dup_ifunc_proc_bug_20950 + assert_normal_exit(<<~RUBY, "[Bug #20950]") + p = { a: 1 }.to_proc + 100.times do + p = p.dup + GC.start + p.call + rescue ArgumentError + end + RUBY + end + + def test_clone_subclass + c1 = Class.new(Proc) + assert_equal c1, c1.new{}.clone.class, '[Bug #17545]' + c1 = Class.new(Proc) {def initialize_clone(*) throw :initialize_clone; end} + assert_throw(:initialize_clone) {c1.new{}.clone} end def test_binding @@ -442,7 +513,7 @@ class TestProc < Test::Unit::TestCase file, lineno = method(:source_location_test).to_proc.binding.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') + assert_equal(@@line_of_source_location_test[0], lineno, 'Bug #2427') end def test_binding_error_unless_ruby_frame @@ -1311,7 +1382,8 @@ class TestProc < Test::Unit::TestCase assert_equal([[:opt, :a], [:rest, :b], [:opt, :c]], proc {|a, *b, c|}.parameters) assert_equal([[:opt, :a], [:rest, :b], [:opt, :c], [:block, :d]], proc {|a, *b, c, &d|}.parameters) assert_equal([[:opt, :a], [:opt, :b], [:rest, :c], [:opt, :d], [:block, :e]], proc {|a, b=:b, *c, d, &e|}.parameters) - assert_equal([[:opt, nil], [:block, :b]], proc {|(a), &b|a}.parameters) + assert_equal([[:opt], [:block, :b]], proc {|(a), &b|a}.parameters) + assert_equal([[:opt], [:rest, :_], [:opt]], proc {|(a_), *_, (b_)|}.parameters) assert_equal([[:opt, :a], [:opt, :b], [:opt, :c], [:opt, :d], [:rest, :e], [:opt, :f], [:opt, :g], [:block, :h]], proc {|a,b,c=:c,d=:d,*e,f,g,&h|}.parameters) assert_equal([[:req]], method(:putc).parameters) @@ -1319,6 +1391,34 @@ class TestProc < Test::Unit::TestCase pr = eval("proc{|"+"(_),"*30+"|}") assert_empty(pr.parameters.map{|_,n|n}.compact) + + assert_equal([[:opt]], proc { it }.parameters) + end + + def test_proc_autosplat_with_multiple_args_with_ruby2_keywords_splat_bug_19759 + def self.yielder_ab(splat) + yield([:a, :b], *splat) + end + + res = yielder_ab([[:aa, :bb], Hash.ruby2_keywords_hash({k: :k})]) do |a, b, k:| + [a, b, k] + end + assert_equal([[:a, :b], [:aa, :bb], :k], res) + + def self.yielder(splat) + yield(*splat) + end + res = yielder([ [:a, :b] ]){|a, b, **| [a, b]} + assert_equal([:a, :b], res) + + res = yielder([ [:a, :b], Hash.ruby2_keywords_hash({}) ]){|a, b, **| [a, b]} + assert_equal([[:a, :b], nil], res) + + res = yielder([ [:a, :b], Hash.ruby2_keywords_hash({c: 1}) ]){|a, b, **| [a, b]} + assert_equal([[:a, :b], nil], res) + + res = yielder([ [:a, :b], Hash.ruby2_keywords_hash({}) ]){|a, b, **nil| [a, b]} + assert_equal([[:a, :b], nil], res) end def test_parameters_lambda @@ -1341,6 +1441,9 @@ class TestProc < Test::Unit::TestCase assert_equal([[:opt, :a]], lambda {|a|}.parameters(lambda: false)) assert_equal([[:opt, :a], [:opt, :b], [:opt, :c], [:opt, :d], [:rest, :e], [:opt, :f], [:opt, :g], [:block, :h]], lambda {|a,b,c=:c,d=:d,*e,f,g,&h|}.parameters(lambda: false)) + + assert_equal([[:req]], proc { it }.parameters(lambda: true)) + assert_equal([[:opt]], lambda { it }.parameters(lambda: false)) end def pm0() end @@ -1396,15 +1499,19 @@ class TestProc < Test::Unit::TestCase assert_include(EnvUtil.labeled_class(name, Proc).new {}.to_s, name) end - @@line_of_source_location_test = __LINE__ + 1 + @@line_of_source_location_test = [__LINE__ + 1, 2, __LINE__ + 3, 5] def source_location_test a=1, b=2 end def test_source_location - file, lineno = method(:source_location_test).source_location + file, *loc = method(:source_location_test).source_location + assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) + assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') + + file, *loc = self.class.instance_method(:source_location_test).source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') + assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') end @@line_of_attr_reader_source_location_test = __LINE__ + 3 @@ -1437,13 +1544,13 @@ class TestProc < Test::Unit::TestCase end def test_block_source_location - exp_lineno = __LINE__ + 3 - file, lineno = block_source_location_test(1, + exp_loc = [__LINE__ + 3, 49, __LINE__ + 4, 49] + file, *loc = block_source_location_test(1, 2, 3) do end assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp_lineno, lineno) + assert_equal(exp_loc, loc) end def test_splat_without_respond_to @@ -1530,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 @@ -1542,6 +1653,274 @@ class TestProc < Test::Unit::TestCase assert_equal(20, b.eval("b")) end + def test_numparam_is_not_local_variables + "foo".tap do + _9 and flunk + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:_9) } + assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } + "bar".tap do + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:_9) } + assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } + end + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:_9) } + assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } + end + + "foo".tap do + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:_9) } + assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } + "bar".tap do + _9 and flunk + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:_9) } + assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } + end + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:_9) } + assert_raise(NameError) { binding.local_variable_set(:_9, 1) } + assert_raise(NameError) { binding.local_variable_defined?(:_9) } + end + end + + def test_implicit_parameters_for_numparams + x = x = 1 + assert_raise(NameError) { binding.implicit_parameter_get(:x) } + assert_raise(NameError) { binding.implicit_parameter_defined?(:x) } + + "foo".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + "bar".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + end + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + end + + "foo".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + "bar".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("bar", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + end + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) + end + end + + def test_it_is_not_local_variable + "foo".tap do + 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 assert_ruby_status([], <<-'end;', '[Bug #13605]', timeout: 30) b = binding @@ -1838,4 +2217,3 @@ class TestProcKeywords < Test::Unit::TestCase assert_raise(ArgumentError) { (f >> g).call(**{})[:a] } end end - diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb index beb181ccd4..b3a88b664c 100644 --- a/test/ruby/test_process.rb +++ b/test/ruby/test_process.rb @@ -23,12 +23,6 @@ class TestProcess < Test::Unit::TestCase return /mswin|mingw|bccwin/ =~ RUBY_PLATFORM end - def write_file(filename, content) - File.open(filename, "w") {|f| - f << content - } - end - def with_tmpchdir Dir.mktmpdir {|d| d = File.realpath(d) @@ -39,7 +33,7 @@ class TestProcess < Test::Unit::TestCase end def run_in_child(str) # should be called in a temporary directory - write_file("test-script", str) + File.write("test-script", str) Process.wait spawn(RUBY, "test-script") $? end @@ -64,8 +58,10 @@ 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 { - write_file 's', <<-"End" + File.write 's', <<-"End" # Too small RLIMIT_NOFILE, such as zero, causes problems. # [OpenBSD] Setting to zero freezes this test. # [GNU/Linux] EINVAL on poll(). EINVAL on ruby's internal poll() ruby with "[ASYNC BUG] thread_timer: select". @@ -120,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') @@ -205,58 +206,67 @@ class TestProcess < Test::Unit::TestCase max = Process.getrlimit(:CORE).last + # When running under ASAN, we need to set disable_coredump=0 for this test; by default + # the ASAN runtime library sets RLIMIT_CORE to 0, "to avoid dumping a 16T+ core file", and + # that inteferes with this test. + asan_options = ENV['ASAN_OPTIONS'] || '' + asan_options << ':' unless asan_options.empty? + env = { + 'ASAN_OPTIONS' => "#{asan_options}disable_coredump=0" + } + n = max - IO.popen([RUBY, "-e", + IO.popen([env, RUBY, "-e", "puts Process.getrlimit(:CORE)", :rlimit_core=>n]) {|io| assert_equal("#{n}\n#{n}\n", io.read) } n = 0 - IO.popen([RUBY, "-e", + IO.popen([env, RUBY, "-e", "puts Process.getrlimit(:CORE)", :rlimit_core=>n]) {|io| assert_equal("#{n}\n#{n}\n", io.read) } n = max - IO.popen([RUBY, "-e", + IO.popen([env, RUBY, "-e", "puts Process.getrlimit(:CORE)", :rlimit_core=>[n]]) {|io| assert_equal("#{n}\n#{n}\n", io.read) } m, n = 0, max - IO.popen([RUBY, "-e", + IO.popen([env, RUBY, "-e", "puts Process.getrlimit(:CORE)", :rlimit_core=>[m,n]]) {|io| assert_equal("#{m}\n#{n}\n", io.read) } m, n = 0, 0 - IO.popen([RUBY, "-e", + IO.popen([env, RUBY, "-e", "puts Process.getrlimit(:CORE)", :rlimit_core=>[m,n]]) {|io| assert_equal("#{m}\n#{n}\n", io.read) } n = max - IO.popen([RUBY, "-e", + IO.popen([env, RUBY, "-e", "puts Process.getrlimit(:CORE), Process.getrlimit(:CPU)", :rlimit_core=>n, :rlimit_cpu=>3600]) {|io| assert_equal("#{n}\n#{n}\n""3600\n3600\n", io.read) } assert_raise(ArgumentError) do - system(RUBY, '-e', 'exit', 'rlimit_bogus'.to_sym => 123) + system(env, RUBY, '-e', 'exit', 'rlimit_bogus'.to_sym => 123) end - assert_separately([],"#{<<~"begin;"}\n#{<<~'end;'}", 'rlimit_cpu'.to_sym => 3600) + assert_separately([env],"#{<<~"begin;"}\n#{<<~'end;'}", 'rlimit_cpu'.to_sym => 3600) BUG = "[ruby-core:82033] [Bug #13744]" begin; assert_equal([3600,3600], Process.getrlimit(:CPU), BUG) end; assert_raise_with_message(ArgumentError, /bogus/) do - system(RUBY, '-e', 'exit', :rlimit_bogus => 123) + system(env, RUBY, '-e', 'exit', :rlimit_bogus => 123) end assert_raise_with_message(ArgumentError, /rlimit_cpu/) { - system(RUBY, '-e', 'exit', "rlimit_cpu\0".to_sym => 3600) + system(env, RUBY, '-e', 'exit', "rlimit_cpu\0".to_sym => 3600) } end @@ -272,21 +282,22 @@ class TestProcess < Test::Unit::TestCase end; end - MANDATORY_ENVS = %w[RUBYLIB RJIT_SEARCH_BUILD_DIR] - case RbConfig::CONFIG['target_os'] - when /linux/ - MANDATORY_ENVS << 'LD_PRELOAD' - when /mswin|mingw/ - MANDATORY_ENVS.concat(%w[HOME USER TMPDIR]) - 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) @@ -361,7 +372,7 @@ class TestProcess < Test::Unit::TestCase def test_execopt_env_path bug8004 = '[ruby-core:53103] [Bug #8004]' Dir.mktmpdir do |d| - open("#{d}/tmp_script.cmd", "w") {|f| f.puts ": ;"; f.chmod(0755)} + File.write("#{d}/tmp_script.cmd", ": ;\n", perm: 0o755) assert_not_nil(pid = Process.spawn({"PATH" => d}, "tmp_script.cmd"), bug8004) wpid, st = Process.waitpid2(pid) assert_equal([pid, true], [wpid, st.success?], bug8004) @@ -399,7 +410,7 @@ class TestProcess < Test::Unit::TestCase def test_execopts_env_popen_string with_tmpchdir do |d| - open('test-script', 'w') do |f| + File.open('test-script', 'w') do |f| ENVCOMMAND.each_with_index do |cmd, i| next if i.zero? or cmd == "-e" f.puts cmd @@ -411,16 +422,14 @@ class TestProcess < Test::Unit::TestCase def test_execopts_preserve_env_on_exec_failure with_tmpchdir {|d| - write_file 's', <<-"End" + File.write 's', <<-"End" ENV["mgg"] = nil prog = "./nonexistent" begin Process.exec({"mgg" => "mggoo"}, [prog, prog]) rescue Errno::ENOENT end - open('out', 'w') {|f| - f.print ENV["mgg"].inspect - } + File.write('out', ENV["mgg"].inspect) End system(RUBY, 's') assert_equal(nil.inspect, File.read('out'), @@ -430,9 +439,7 @@ class TestProcess < Test::Unit::TestCase def test_execopts_env_single_word with_tmpchdir {|d| - open("test_execopts_env_single_word.rb", "w") {|f| - f.puts "print ENV['hgga']" - } + File.write("test_execopts_env_single_word.rb", "print ENV['hgga']\n") system({"hgga"=>"ugu"}, RUBY, :in => 'test_execopts_env_single_word.rb', :out => 'test_execopts_env_single_word.out') @@ -554,7 +561,7 @@ class TestProcess < Test::Unit::TestCase assert_equal("a", File.read("out").chomp) if windows? # currently telling to child the file modes is not supported. - open("out", "a") {|f| f.write "0\n"} + File.write("out", "0\n", mode: "a") else Process.wait Process.spawn(*ECHO["0"], STDOUT=>["out", File::WRONLY|File::CREAT|File::APPEND, 0644]) assert_equal("a\n0\n", File.read("out")) @@ -665,6 +672,7 @@ class TestProcess < Test::Unit::TestCase end unless windows? # does not support fifo def test_execopts_redirect_open_fifo_interrupt_raise + pid = nil with_tmpchdir {|d| begin File.mkfifo("fifo") @@ -682,15 +690,21 @@ class TestProcess < Test::Unit::TestCase puts "ok" end EOS + pid = io.pid assert_equal("start\n", io.gets) sleep 0.5 Process.kill(:USR1, io.pid) assert_equal("ok\n", io.read) } + assert_equal(pid, $?.pid) + assert_predicate($?, :success?) } + ensure + assert_raise(Errno::ESRCH) {Process.kill(:KILL, pid)} if pid end unless windows? # does not support fifo def test_execopts_redirect_open_fifo_interrupt_print + pid = nil with_tmpchdir {|d| begin File.mkfifo("fifo") @@ -703,14 +717,25 @@ class TestProcess < Test::Unit::TestCase puts "start" system("cat", :in => "fifo") EOS + pid = io.pid assert_equal("start\n", io.gets) sleep 0.2 # wait for the child to stop at opening "fifo" Process.kill(:USR1, io.pid) assert_equal("trap\n", io.readpartial(8)) + sleep 0.2 # wait for the child to return to opening "fifo". + # On arm64-darwin22, often deadlocks while the child is + # opening "fifo". Not sure to where "ok" line being written + # at the next has gone. File.write("fifo", "ok\n") assert_equal("ok\n", io.read) } + assert_equal(pid, $?.pid) + assert_predicate($?, :success?) } + ensure + if pid + assert_raise(Errno::ESRCH) {Process.kill(:KILL, pid)} + end end unless windows? # does not support fifo def test_execopts_redirect_pipe @@ -858,7 +883,7 @@ class TestProcess < Test::Unit::TestCase def test_execopts_exec with_tmpchdir {|d| - write_file("s", 'exec "echo aaa", STDOUT=>"foo"') + File.write("s", 'exec "echo aaa", STDOUT=>"foo"') pid = spawn RUBY, 's' Process.wait pid assert_equal("aaa\n", File.read("foo")) @@ -905,15 +930,29 @@ class TestProcess < Test::Unit::TestCase } end - def test_popen_fork - IO.popen("-") {|io| - if !io - puts "fooo" - else - assert_equal("fooo\n", io.read) + if Process.respond_to?(:fork) + def test_popen_fork + IO.popen("-") do |io| + if !io + puts "fooo" + else + assert_equal("fooo\n", io.read) + end end - } - rescue NotImplementedError + end + + def test_popen_fork_ensure + IO.popen("-") do |io| + if !io + STDERR.reopen(STDOUT) + raise "fooo" + else + assert_empty io.read + end + end + rescue RuntimeError + abort "[Bug #20995] should not reach here" + end end def test_fd_inheritance @@ -932,7 +971,7 @@ class TestProcess < Test::Unit::TestCase } with_pipe {|r, w| with_tmpchdir {|d| - write_file("s", <<-"End") + File.write("s", <<-"End") exec(#{RUBY.dump}, '-e', 'IO.new(ARGV[0].to_i, "w").puts("bu") rescue nil', #{w.fileno.to_s.dump}, :close_others=>false) @@ -986,7 +1025,7 @@ class TestProcess < Test::Unit::TestCase assert_equal("bi\n", r.read) } with_pipe {|r, w| - write_file("s", <<-"End") + File.write("s", <<-"End") exec(#{RUBY.dump}, '-e', 'STDERR.reopen("err", "w"); IO.new(ARGV[0].to_i, "w").puts("mu")', #{w.fileno.to_s.dump}, @@ -1112,7 +1151,7 @@ class TestProcess < Test::Unit::TestCase def test_exec_noshell with_tmpchdir {|d| - write_file("s", <<-"End") + File.write("s", <<-"End") str = "echo non existing command name which contains spaces" STDERR.reopen(STDOUT) begin @@ -1128,7 +1167,7 @@ class TestProcess < Test::Unit::TestCase def test_system_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') File.open("result", "w") {|t| t << "haha pid=#{$$} ppid=#{Process.ppid}" } exit 5 End @@ -1144,7 +1183,7 @@ class TestProcess < Test::Unit::TestCase def test_spawn_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') File.open("result", "w") {|t| t << "hihi pid=#{$$} ppid=#{Process.ppid}" } exit 6 End @@ -1161,7 +1200,7 @@ class TestProcess < Test::Unit::TestCase def test_popen_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') print "fufu pid=#{$$} ppid=#{Process.ppid}" exit 7 End @@ -1180,7 +1219,7 @@ class TestProcess < Test::Unit::TestCase def test_popen_wordsplit_beginning_and_trailing_spaces with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') print "fufumm pid=#{$$} ppid=#{Process.ppid}" exit 7 End @@ -1199,7 +1238,7 @@ class TestProcess < Test::Unit::TestCase def test_exec_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') File.open("result", "w") {|t| if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM t << "hehe ppid=#{Process.ppid}" @@ -1209,7 +1248,7 @@ class TestProcess < Test::Unit::TestCase } exit 6 End - write_file("s", <<-"End") + File.write("s", <<-"End") ruby = #{RUBY.dump} exec "\#{ruby} script" End @@ -1230,11 +1269,11 @@ class TestProcess < Test::Unit::TestCase def test_system_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') File.open("result1", "w") {|t| t << "taka pid=#{$$} ppid=#{Process.ppid}" } exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') File.open("result2", "w") {|t| t << "taki pid=#{$$} ppid=#{Process.ppid}" } exit 8 End @@ -1250,7 +1289,7 @@ class TestProcess < Test::Unit::TestCase if windows? Dir.mkdir(path = "path with space") - write_file(bat = path + "/bat test.bat", "@echo %1>out") + File.write(bat = path + "/bat test.bat", "@echo %1>out") system(bat, "foo 'bar'") assert_equal(%["foo 'bar'"\n], File.read("out"), '[ruby-core:22960]') system(%[#{bat.dump} "foo 'bar'"]) @@ -1261,11 +1300,11 @@ class TestProcess < Test::Unit::TestCase def test_spawn_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') File.open("result1", "w") {|t| t << "taku pid=#{$$} ppid=#{Process.ppid}" } exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') File.open("result2", "w") {|t| t << "take pid=#{$$} ppid=#{Process.ppid}" } exit 8 End @@ -1282,7 +1321,7 @@ class TestProcess < Test::Unit::TestCase if windows? Dir.mkdir(path = "path with space") - write_file(bat = path + "/bat test.bat", "@echo %1>out") + File.write(bat = path + "/bat test.bat", "@echo %1>out") pid = spawn(bat, "foo 'bar'") Process.wait pid status = $? @@ -1301,11 +1340,11 @@ class TestProcess < Test::Unit::TestCase def test_popen_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') puts "tako pid=#{$$} ppid=#{Process.ppid}" exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') puts "tika pid=#{$$} ppid=#{Process.ppid}" exit 8 End @@ -1320,7 +1359,7 @@ class TestProcess < Test::Unit::TestCase if windows? Dir.mkdir(path = "path with space") - write_file(bat = path + "/bat test.bat", "@echo %1") + File.write(bat = path + "/bat test.bat", "@echo %1") r = IO.popen([bat, "foo 'bar'"]) {|f| f.read} assert_equal(%["foo 'bar'"\n], r, '[ruby-core:22960]') r = IO.popen(%[#{bat.dump} "foo 'bar'"]) {|f| f.read} @@ -1331,15 +1370,15 @@ class TestProcess < Test::Unit::TestCase def test_exec_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') File.open("result1", "w") {|t| t << "tiki pid=#{$$} ppid=#{Process.ppid}" } exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') File.open("result2", "w") {|t| t << "tiku pid=#{$$} ppid=#{Process.ppid}" } exit 8 End - write_file("s", <<-"End") + File.write("s", <<-"End") ruby = #{RUBY.dump} exec("\#{ruby} script1 || \#{ruby} script2") End @@ -1366,7 +1405,7 @@ class TestProcess < Test::Unit::TestCase assert_equal("1", IO.popen([[RUBY, "qwerty"], "-e", "print 1"]) {|f| f.read }) - write_file("s", <<-"End") + File.write("s", <<-"End") exec([#{RUBY.dump}, "lkjh"], "-e", "exit 5") End pid = spawn RUBY, "s" @@ -1376,7 +1415,7 @@ class TestProcess < Test::Unit::TestCase end def with_stdin(filename) - open(filename) {|f| + File.open(filename) {|f| begin old = STDIN.dup begin @@ -1393,8 +1432,8 @@ class TestProcess < Test::Unit::TestCase def test_argv0_noarg with_tmpchdir {|d| - open("t", "w") {|f| f.print "exit true" } - open("f", "w") {|f| f.print "exit false" } + File.write("t", "exit true") + File.write("f", "exit false") with_stdin("t") { assert_equal(true, system([RUBY, "qaz"])) } with_stdin("f") { assert_equal(false, system([RUBY, "wsx"])) } @@ -1424,6 +1463,11 @@ class TestProcess < Test::Unit::TestCase REPRO end + def test_argv0_frozen + assert_predicate Process.argv0, :frozen? + assert_predicate $0, :frozen? + end + def test_status with_tmpchdir do s = run_in_child("exit 1") @@ -1432,8 +1476,6 @@ class TestProcess < Test::Unit::TestCase assert_equal(s, s) assert_equal(s, s.to_i) - assert_equal(s.to_i & 0x55555555, s & 0x55555555) - assert_equal(s.to_i >> 1, s >> 1) assert_equal(false, s.stopped?) assert_equal(nil, s.stopsig) @@ -1449,7 +1491,7 @@ class TestProcess < Test::Unit::TestCase expected = Signal.list.include?("QUIT") ? [false, true, false, nil] : [true, false, false, true] with_tmpchdir do - write_file("foo", "Process.kill(:KILL, $$); exit(42)") + File.write("foo", "Process.kill(:KILL, $$); exit(42)") system(RUBY, "foo") s = $? assert_equal(expected, @@ -1497,7 +1539,7 @@ class TestProcess < Test::Unit::TestCase def test_wait_without_arg with_tmpchdir do - write_file("foo", "sleep 0.1") + File.write("foo", "sleep 0.1") pid = spawn(RUBY, "foo") assert_equal(pid, Process.wait) end @@ -1505,7 +1547,7 @@ class TestProcess < Test::Unit::TestCase def test_wait2 with_tmpchdir do - write_file("foo", "sleep 0.1") + File.write("foo", "sleep 0.1") pid = spawn(RUBY, "foo") assert_equal([pid, 0], Process.wait2) end @@ -1513,7 +1555,7 @@ class TestProcess < Test::Unit::TestCase def test_waitall with_tmpchdir do - write_file("foo", "sleep 0.1") + File.write("foo", "sleep 0.1") ps = (0...3).map { spawn(RUBY, "foo") }.sort ss = Process.waitall.sort ps.zip(ss) do |p1, (p2, s)| @@ -1546,6 +1588,8 @@ class TestProcess < Test::Unit::TestCase assert_operator(diff, :<, sec, ->{"#{bug11340}: #{diff} seconds to interrupt Process.wait"}) f.puts + rescue Errno::EPIPE + omit "child process exited already in #{diff} seconds" end end @@ -1553,7 +1597,7 @@ class TestProcess < Test::Unit::TestCase with_tmpchdir do s = run_in_child("abort") assert_not_predicate(s, :success?) - write_file("test-script", "#{<<~"begin;"}\n#{<<~'end;'}") + File.write("test-script", "#{<<~"begin;"}\n#{<<~'end;'}") begin; STDERR.reopen(STDOUT) begin @@ -1646,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 @@ -1657,12 +1702,7 @@ class TestProcess < Test::Unit::TestCase if g = Etc.getgrgid(Process.gid) assert_equal(Process.gid, Process::GID.from_name(g.name), g.name) end - expected_excs = [ArgumentError] - expected_excs << Errno::ENOENT if defined?(Errno::ENOENT) - expected_excs << Errno::ESRCH if defined?(Errno::ESRCH) # WSL 2 actually raises Errno::ESRCH - expected_excs << Errno::EBADF if defined?(Errno::EBADF) - expected_excs << Errno::EPERM if defined?(Errno::EPERM) - exc = assert_raise(*expected_excs) do + exc = assert_raise_kind_of(ArgumentError, SystemCallError) do Process::GID.from_name("\u{4e0d 5b58 5728}") # fu son zai ("absent" in Kanji) end assert_match(/\u{4e0d 5b58 5728}/, exc.message) if exc.is_a?(ArgumentError) @@ -1694,11 +1734,6 @@ class TestProcess < Test::Unit::TestCase end def test_wait_and_sigchild - if /freebsd|openbsd/ =~ RUBY_PLATFORM - # this relates #4173 - # When ruby can use 2 cores, signal and wait4 may miss the signal. - omit "this fails on FreeBSD and OpenBSD on multithreaded environment" - end signal_received = [] IO.pipe do |sig_r, sig_w| Signal.trap(:CHLD) do @@ -1706,22 +1741,23 @@ class TestProcess < Test::Unit::TestCase sig_w.write('?') end pid = nil + th = nil IO.pipe do |r, w| pid = fork { r.read(1); exit } - Thread.start { + th = Thread.start { Thread.current.report_on_exception = false raise } w.puts end Process.wait pid + begin + th.join + rescue Exception + end assert_send [sig_r, :wait_readable, 5], 'self-pipe not readable' end - if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # checking -DRJIT_FORCE_ENABLE. It may trigger extra SIGCHLD. - assert_equal [true], signal_received.uniq, "[ruby-core:19744]" - else - assert_equal [true], signal_received, "[ruby-core:19744]" - end + assert_equal [true], signal_received, "[ruby-core:19744]" rescue NotImplementedError, ArgumentError ensure begin @@ -1731,15 +1767,12 @@ class TestProcess < Test::Unit::TestCase end def test_no_curdir - if /solaris/i =~ RUBY_PLATFORM - omit "Temporary omit to avoid CI failures after commit to use realpath on required files" - end with_tmpchdir {|d| Dir.mkdir("vd") status = nil Dir.chdir("vd") { dir = "#{d}/vd" - # OpenSolaris cannot remove the current directory. + # Windows cannot remove the current directory with permission issues. system(RUBY, "--disable-gems", "-e", "Dir.chdir '..'; Dir.rmdir #{dir.dump}", err: File::NULL) system({"RUBYLIB"=>nil}, RUBY, "--disable-gems", "-e", "exit true") status = $? @@ -1751,16 +1784,16 @@ class TestProcess < Test::Unit::TestCase def test_fallback_to_sh feature = '[ruby-core:32745]' with_tmpchdir do |d| - open("tmp_script.#{$$}", "w") {|f| f.puts ": ;"; f.chmod(0755)} + File.write("tmp_script.#{$$}", ": ;\n", perm: 0o755) assert_not_nil(pid = Process.spawn("./tmp_script.#{$$}"), feature) wpid, st = Process.waitpid2(pid) assert_equal([pid, true], [wpid, st.success?], feature) - open("tmp_script.#{$$}", "w") {|f| f.puts "echo $#: $@"; f.chmod(0755)} + File.write("tmp_script.#{$$}", "echo $#: $@", perm: 0o755) result = IO.popen(["./tmp_script.#{$$}", "a b", "c"]) {|f| f.read} assert_equal("2: a b c\n", result, feature) - open("tmp_script.#{$$}", "w") {|f| f.puts "echo $hghg"; f.chmod(0755)} + File.write("tmp_script.#{$$}", "echo $hghg", perm: 0o755) result = IO.popen([{"hghg" => "mogomogo"}, "./tmp_script.#{$$}", "a b", "c"]) {|f| f.read} assert_equal("mogomogo\n", result, feature) @@ -1773,9 +1806,6 @@ class TestProcess < Test::Unit::TestCase end def test_aspawn_too_long_path - if /solaris/i =~ RUBY_PLATFORM && !defined?(Process::RLIMIT_NPROC) - omit "Too exhaustive test on platforms without Process::RLIMIT_NPROC such as Solaris 10" - end bug4315 = '[ruby-core:34833] #7904 [ruby-core:52628] #11613' assert_fail_too_long_path(%w"echo |", bug4315) end @@ -1788,14 +1818,20 @@ class TestProcess < Test::Unit::TestCase exs << Errno::EINVAL if windows? exs << Errno::E2BIG if defined?(Errno::E2BIG) opts = {[STDOUT, STDERR]=>File::NULL} - opts[:rlimit_nproc] = 128 if defined?(Process::RLIMIT_NPROC) + if defined?(Process::RLIMIT_NPROC) + opts[:rlimit_nproc] = /openbsd/i =~ RUBY_PLATFORM ? 64 : 128 + end EnvUtil.suppress_warning do assert_raise(*exs, mesg) do begin loop do Process.spawn(cmds.join(sep), opts) min = [cmds.size, min].max - cmds *= 100 + begin + cmds *= 100 + rescue ArgumentError + raise NoMemoryError + end end rescue NoMemoryError size = cmds.size @@ -1960,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 @@ -2137,7 +2173,7 @@ EOS "c\u{1EE7}a", ].each do |arg| begin - arg = arg.encode(Encoding.find("locale")) + arg = arg.encode(Encoding.local_charmap) rescue else assert_in_out_err([], "#{<<-"begin;"}\n#{<<-"end;"}", [arg], [], bug12841) @@ -2155,7 +2191,9 @@ EOS t3 = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) assert_operator(t1, :<=, t2) assert_operator(t2, :<=, t3) - assert_raise(Errno::EINVAL) { Process.clock_gettime(:foo) } + assert_raise_with_message(Errno::EINVAL, /:foo/) do + Process.clock_gettime(:foo) + end end def test_clock_gettime_unit @@ -2260,7 +2298,9 @@ EOS rescue Errno::EINVAL else assert_kind_of(Integer, r) - assert_raise(Errno::EINVAL) { Process.clock_getres(:foo) } + assert_raise_with_message(Errno::EINVAL, /:foo/) do + Process.clock_getres(:foo) + end end def test_clock_getres_constants @@ -2347,7 +2387,7 @@ EOS end def test_deadlock_by_signal_at_forking - assert_separately(%W(--disable=gems - #{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 @@ -2586,6 +2626,26 @@ EOS end end if Process.respond_to?(:_fork) + def test__fork_pid_cache + _parent_pid = Process.pid + r, w = IO.pipe + pid = Process._fork + if pid == 0 + begin + r.close + w << "ok: #{Process.pid}" + w.close + ensure + exit! + end + else + w.close + assert_equal("ok: #{pid}", r.read) + r.close + Process.waitpid(pid) + end + end if Process.respond_to?(:_fork) + def test__fork_hook %w(fork Process.fork).each do |method| feature17795 = '[ruby-core:103400] [Feature #17795]' @@ -2658,4 +2718,147 @@ EOS end end; end if Process.respond_to?(:_fork) + + def test_warmup_promote_all_objects_to_oldgen + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + require 'objspace' + begin; + obj = Object.new + + assert_not_include(ObjectSpace.dump(obj), '"old":true') + Process.warmup + assert_include(ObjectSpace.dump(obj), '"old":true') + end; + end + + def test_warmup_run_major_gc_and_compact + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # Run a GC to ensure that we are not in the middle of a GC run + GC.start + + major_gc_count = GC.stat(:major_gc_count) + compact_count = GC.stat(:compact_count) + Process.warmup + assert_equal major_gc_count + 1, GC.stat(:major_gc_count) + assert_equal compact_count + 1, GC.stat(:compact_count) + end; + end + + def test_warmup_precompute_string_coderange + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + require 'objspace' + begin; + obj = "a" * 12 + obj.force_encoding(Encoding::UTF_16LE) + obj.force_encoding(Encoding::BINARY) + assert_include(ObjectSpace.dump(obj), '"coderange":"unknown"') + Process.warmup + assert_include(ObjectSpace.dump(obj), '"coderange":"7bit"') + end; + end + + def test_warmup_frees_pages + assert_separately([{"RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO" => "1.0"}, "-W0"], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + GC.start + + TIMES = 100_000 + ary = Array.new(TIMES) + TIMES.times do |i| + ary[i] = Object.new + end + ary.clear + ary = nil + + # Disable GC so we can make sure GC only runs in Process.warmup + GC.disable + + total_slots_before = GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_slots) + + Process.warmup + + # 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; + end + + def test_concurrent_group_and_pid_wait + # Use a pair of pipes that will make long_pid exit when this test exits, to avoid + # leaking temp processes. + long_rpipe, long_wpipe = IO.pipe + short_rpipe, short_wpipe = IO.pipe + # This process should run forever + long_pid = fork do + [short_rpipe, short_wpipe, long_wpipe].each(&:close) + long_rpipe.read + end + # This process will exit + short_pid = fork do + [long_rpipe, long_wpipe, short_wpipe].each(&:close) + short_rpipe.read + end + t1, t2, t3 = nil + EnvUtil.timeout(5) do + t1 = Thread.new do + Process.waitpid long_pid + end + # Wait for us to be blocking in a call to waitpid2 + Thread.pass until t1.stop? + short_wpipe.close # Make short_pid exit + + # The short pid has exited, so -1 should pick that up. + assert_equal short_pid, Process.waitpid(-1) + + # Terminate t1 for the next phase of the test. + t1.kill + t1.join + + t2 = Thread.new do + Process.waitpid(-1) + rescue Errno::ECHILD + nil + end + Thread.pass until t2.stop? + t3 = Thread.new do + Process.waitpid long_pid + rescue Errno::ECHILD + nil + end + Thread.pass until t3.stop? + + # it's actually nondeterministic which of t2 or t3 will receive the wait (this + # nondeterminism comes from the behaviour of the underlying system calls) + long_wpipe.close + assert_equal [long_pid], [t2, t3].map(&:value).compact + end + ensure + [t1, t2, t3].each { _1&.kill rescue nil } + [t1, t2, t3].each { _1&.join rescue nil } + [long_rpipe, long_wpipe, short_rpipe, short_wpipe].each { _1&.close rescue nil } + end if defined?(fork) + + def test_handle_interrupt_with_fork + Thread.handle_interrupt(RuntimeError => :never) do + Thread.current.raise(RuntimeError, "Queued error") + + assert_predicate Thread, :pending_interrupt? + + pid = Process.fork do + if Thread.pending_interrupt? + exit 1 + end + end + + _, status = Process.waitpid2(pid) + assert_predicate status, :success? + + assert_predicate Thread, :pending_interrupt? + end + rescue RuntimeError + # Ignore. + end if defined?(fork) end diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb new file mode 100644 index 0000000000..6ae511217a --- /dev/null +++ b/test/ruby/test_ractor.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: false +require 'test/unit' + +class TestRactor < Test::Unit::TestCase + def test_shareability_of_iseq_proc + assert_raise Ractor::IsolationError do + foo = [] + Ractor.shareable_proc{ foo } + end + 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/) + + x = str.instance_exec { method(:to_s) } + assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()>", exception: Ractor::Error) + + x = str.instance_exec { method(:to_s).to_proc } + assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()>", exception: Ractor::Error) + + x = str.instance_exec { method(:itself).to_proc } + assert_unshareable(x, "can not make shareable object for #<Method: String(Kernel)#itself()>", exception: Ractor::Error) + + str.freeze + + x = str.instance_exec { proc { to_s } } + assert_make_shareable(x) + + x = str.instance_exec { method(:to_s) } + assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()>", exception: Ractor::Error) + + x = str.instance_exec { method(:to_s).to_proc } + assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()>", exception: Ractor::Error) + + 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 + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + Warning[:experimental] = false + + main_ractor_id = Thread.current.group.object_id + 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) + assert Ractor.shareable?(obj), "object didn't become shareable" + end + + def assert_unshareable(obj, msg=nil, exception: Ractor::IsolationError) + refute Ractor.shareable?(obj), "object is already shareable" + assert_raise_with_message(exception, msg) do + Ractor.make_shareable(obj) + end + refute Ractor.shareable?(obj), "despite raising, object became shareable" + end +end diff --git a/test/ruby/test_rand.rb b/test/ruby/test_rand.rb index a4beffd689..f177664943 100644 --- a/test/ruby/test_rand.rb +++ b/test/ruby/test_rand.rb @@ -434,4 +434,9 @@ class TestRand < Test::Unit::TestCase # probability of failure <= 1/256**8 assert_operator(size.fdiv(n), :>, 15) end + + def test_broken_marshal + assert_raise(ArgumentError) { Marshal.load("\x04\bU:\vRandom" + Marshal.dump([1,0,1])[2..]) } + assert_raise(ArgumentError) { Marshal.load("\x04\bU:\vRandom" + Marshal.dump([1,-1,1])[2..]) } + end end diff --git a/test/ruby/test_random_formatter.rb b/test/ruby/test_random_formatter.rb index a5072099e1..784fbfc2b8 100644 --- a/test/ruby/test_random_formatter.rb +++ b/test/ruby/test_random_formatter.rb @@ -75,6 +75,47 @@ module Random::Formatter assert_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, uuid) end + def assert_uuid_v7(**opts) + t1 = current_uuid7_time(**opts) + uuid = @it.uuid_v7(**opts) + t3 = current_uuid7_time(**opts) + + assert_match(/\A\h{8}-\h{4}-7\h{3}-[89ab]\h{3}-\h{12}\z/, uuid) + + t2 = get_uuid7_time(uuid, **opts) + assert_operator(t1, :<=, t2) + assert_operator(t2, :<=, t3) + end + + def test_uuid_v7 + assert_uuid_v7 + 0.upto(12) do |extra_timestamp_bits| + assert_uuid_v7 extra_timestamp_bits: extra_timestamp_bits + end + end + + # It would be nice to simply use Time#floor here. But that is problematic + # due to the difference between decimal vs binary fractions. + def current_uuid7_time(extra_timestamp_bits: 0) + denominator = (1 << extra_timestamp_bits).to_r + Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) + .then {|ns| ((ns / 1_000_000r) * denominator).floor / denominator } + .then {|ms| Time.at(ms / 1000r, in: "+00:00") } + end + + def get_uuid7_time(uuid, extra_timestamp_bits: 0) + denominator = (1 << extra_timestamp_bits) * 1000r + extra_chars = extra_timestamp_bits / 4 + last_char_bits = extra_timestamp_bits % 4 + extra_chars += 1 if last_char_bits != 0 + timestamp_re = /\A(\h{8})-(\h{4})-7(\h{#{extra_chars}})/ + timestamp_chars = uuid.match(timestamp_re).captures.join + timestamp = timestamp_chars.to_i(16) + timestamp >>= 4 - last_char_bits unless last_char_bits == 0 + timestamp /= denominator + Time.at timestamp, in: "+00:00" + end + def test_alphanumeric 65.times do |n| an = @it.alphanumeric(n) @@ -83,6 +124,20 @@ module Random::Formatter end end + def test_alphanumeric_chars + [ + [[*"0".."9"], /\A\d*\z/], + [[*"a".."t"], /\A[a-t]*\z/], + ["一二三四五å…七八ä¹å".chars, /\A[一二三四五å…七八ä¹å]*\z/], + ].each do |chars, pattern| + 10.times do |n| + an = @it.alphanumeric(n, chars: chars) + assert_match(pattern, an) + assert_equal(n, an.length) + end + end + end + def assert_in_range(range, result, mesg = nil) assert(range.cover?(result), build_message(mesg, "Expected #{result} to be in #{range}")) end @@ -110,6 +165,11 @@ module Random::Formatter def setup @it = Random end + + def test_alphanumeric_frozen + assert_predicate @it::Formatter::ALPHANUMERIC, :frozen? + assert @it::Formatter::ALPHANUMERIC.all?(&:frozen?) + end end class TestInstanceMethods < Test::Unit::TestCase diff --git a/test/ruby/test_range.rb b/test/ruby/test_range.rb index 0a131644e5..ff17dca69e 100644 --- a/test/ruby/test_range.rb +++ b/test/ruby/test_range.rb @@ -2,7 +2,7 @@ require 'test/unit' require 'delegate' require 'timeout' -require 'bigdecimal' +require 'date' require 'rbconfig/sizeof' class TestRange < Test::Unit::TestCase @@ -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 @@ -121,13 +122,15 @@ class TestRange < Test::Unit::TestCase assert_equal([10,9,8], (0..10).max(3)) assert_equal([9,8,7], (0...10).max(3)) + assert_equal([10,9,8], (..10).max(3)) + assert_equal([9,8,7], (...10).max(3)) assert_raise(RangeError) { (1..).max(3) } assert_raise(RangeError) { (1...).max(3) } assert_raise(RangeError) { (..0).min {|a, b| a <=> b } } assert_equal(2, (..2).max) - assert_raise(TypeError) { (...2).max } + assert_equal(1, (...2).max) assert_raise(TypeError) { (...2.0).max } assert_equal(Float::INFINITY, (1..Float::INFINITY).max) @@ -246,67 +249,138 @@ class TestRange < Test::Unit::TestCase assert_kind_of(String, (0..1).hash.to_s) end - def test_step - a = [] - (0..10).step {|x| a << x } - assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], a) + def test_step_numeric_range + # Fixnums, floats and all other numbers (like rationals) should behave exactly the same, + # but the behavior is implemented independently in 3 different branches of code, + # so we need to test each of them. + %i[to_i to_r to_f].each do |type| + conv = type.to_proc - a = [] - (0..).step {|x| a << x; break if a.size == 10 } - assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], a) + from = conv.(0) + to = conv.(10) + step = conv.(2) - a = [] - (0..10).step(2) {|x| a << x } - assert_equal([0, 2, 4, 6, 8, 10], a) + # finite + a = [] + (from..to).step(step) {|x| a << x } + assert_equal([0, 2, 4, 6, 8, 10].map(&conv), a) - a = [] - (0..).step(2) {|x| a << x; break if a.size == 10 } - assert_equal([0, 2, 4, 6, 8, 10, 12, 14, 16, 18], a) - - assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step) - assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step(2)) - assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step(0.5)) - assert_kind_of(Enumerator::ArithmeticSequence, (10..0).step(-1)) - assert_kind_of(Enumerator::ArithmeticSequence, (..10).step(2)) - assert_kind_of(Enumerator::ArithmeticSequence, (1..).step(2)) - - assert_raise(ArgumentError) { (0..10).step(-1) { } } - assert_raise(ArgumentError) { (0..10).step(0) } - assert_raise(ArgumentError) { (0..10).step(0) { } } - assert_raise(ArgumentError) { (0..).step(-1) { } } - assert_raise(ArgumentError) { (0..).step(0) } - assert_raise(ArgumentError) { (0..).step(0) { } } + a = [] + (from...to).step(step) {|x| a << x } + assert_equal([0, 2, 4, 6, 8].map(&conv), a) - a = [] - ("a" .. "z").step(2) {|x| a << x } - assert_equal(%w(a c e g i k m o q s u w y), a) + # Note: ArithmeticSequence behavior tested in its own test, but we also put it here + # to demonstrate the result is the same + assert_kind_of(Enumerator::ArithmeticSequence, (from..to).step(step)) + assert_equal([0, 2, 4, 6, 8, 10].map(&conv), (from..to).step(step).to_a) + assert_kind_of(Enumerator::ArithmeticSequence, (from...to).step(step)) + assert_equal([0, 2, 4, 6, 8].map(&conv), (from...to).step(step).to_a) - a = [] - ("a" .. ).step(2) {|x| a << x; break if a.size == 13 } - assert_equal(%w(a c e g i k m o q s u w y), a) + # endless + a = [] + (from..).step(step) {|x| a << x; break if a.size == 5 } + assert_equal([0, 2, 4, 6, 8].map(&conv), a) - a = [] - ("a" .. "z").step(2**32) {|x| a << x } - assert_equal(["a"], a) + assert_kind_of(Enumerator::ArithmeticSequence, (from..).step(step)) + assert_equal([0, 2, 4, 6, 8].map(&conv), (from..).step(step).take(5)) - a = [] - (:a .. :z).step(2) {|x| a << x } - assert_equal(%i(a c e g i k m o q s u w y), a) + # beginless + assert_raise(ArgumentError) { (..to).step(step) {} } + assert_kind_of(Enumerator::ArithmeticSequence, (..to).step(step)) + # This is inconsistent, but so it is implemented by ArithmeticSequence + assert_raise(TypeError) { (..to).step(step).to_a } - a = [] - (:a .. ).step(2) {|x| a << x; break if a.size == 13 } - assert_equal(%i(a c e g i k m o q s u w y), a) + # negative step - a = [] - (:a .. :z).step(2**32) {|x| a << x } - assert_equal([:a], a) + a = [] + (from..to).step(-step) {|x| a << x } + assert_equal([], a) + + a = [] + (from..-to).step(-step) {|x| a << x } + assert_equal([0, -2, -4, -6, -8, -10].map(&conv), a) + + a = [] + (from...-to).step(-step) {|x| a << x } + assert_equal([0, -2, -4, -6, -8].map(&conv), a) + + a = [] + (from...).step(-step) {|x| a << x; break if a.size == 5 } + assert_equal([0, -2, -4, -6, -8].map(&conv), a) + + assert_kind_of(Enumerator::ArithmeticSequence, (from..to).step(-step)) + assert_equal([], (from..to).step(-step).to_a) + + assert_kind_of(Enumerator::ArithmeticSequence, (from..-to).step(-step)) + assert_equal([0, -2, -4, -6, -8, -10].map(&conv), (from..-to).step(-step).to_a) + + assert_kind_of(Enumerator::ArithmeticSequence, (from...-to).step(-step)) + assert_equal([0, -2, -4, -6, -8].map(&conv), (from...-to).step(-step).to_a) + + assert_kind_of(Enumerator::ArithmeticSequence, (from...).step(-step)) + assert_equal([0, -2, -4, -6, -8].map(&conv), (from...).step(-step).take(5)) + + # zero step + + assert_raise(ArgumentError) { (from..to).step(0) {} } + assert_raise(ArgumentError) { (from..to).step(0) } + + # default step + + a = [] + (from..to).step {|x| a << x } + assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(&conv), a) + + assert_kind_of(Enumerator::ArithmeticSequence, (from..to).step) + assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(&conv), (from..to).step.to_a) + + # default + endless range + a = [] + (from..).step {|x| a << x; break if a.size == 5 } + assert_equal([0, 1, 2, 3, 4].map(&conv), a) + + assert_kind_of(Enumerator::ArithmeticSequence, (from..).step) + assert_equal([0, 1, 2, 3, 4].map(&conv), (from..).step.take(5)) + # default + beginless range + assert_kind_of(Enumerator::ArithmeticSequence, (..to).step) + + # step is not numeric + + to = conv.(5) + + val = Struct.new(:val) + + a = [] + assert_raise(TypeError) { (from..to).step(val.new(step)) {|x| a << x } } + assert_kind_of(Enumerator, (from..to).step(val.new(step))) + assert_raise(TypeError) { (from..to).step(val.new(step)).to_a } + + # step is not numeric, but coercible + val = Struct.new(:val) do + def coerce(num) = [self.class.new(num), self] + def +(other) = self.class.new(val + other.val) + def <=>(other) = other.is_a?(self.class) ? val <=> other.val : val <=> other + end + + a = [] + (from..to).step(val.new(step)) {|x| a << x } + assert_equal([from, val.new(conv.(2)), val.new(conv.(4))], a) + + assert_kind_of(Enumerator, (from..to).step(val.new(step))) + assert_equal([from, val.new(conv.(2)), val.new(conv.(4))], (from..to).step(val.new(step)).to_a) + end + end + + def test_step_numeric_fixnum_boundary a = [] (2**32-1 .. 2**32+1).step(2) {|x| a << x } assert_equal([4294967295, 4294967297], a) + zero = (2**32).coerce(0).first assert_raise(ArgumentError) { (2**32-1 .. 2**32+1).step(zero) } assert_raise(ArgumentError) { (2**32-1 .. 2**32+1).step(zero) { } } + a = [] (2**32-1 .. ).step(2) {|x| a << x; break if a.size == 2 } assert_equal([4294967295, 4294967297], a) @@ -315,58 +389,189 @@ class TestRange < Test::Unit::TestCase a = [] (max..).step {|x| a << x; break if a.size == 2 } assert_equal([max, max+1], a) + a = [] (max..).step(max) {|x| a << x; break if a.size == 4 } assert_equal([max, 2*max, 3*max, 4*max], a) + end - o1 = Object.new - o2 = Object.new - def o1.<=>(x); -1; end - def o2.<=>(x); 0; end - assert_raise(TypeError) { (o1..o2).step(1) { } } - assert_raise(TypeError) { (o1..).step(1) { } } + def test_step_big_float + a = [] + (0x40000000..0x40000002).step(0.5) {|x| a << x } + assert_equal([1073741824, 1073741824.5, 1073741825.0, 1073741825.5, 1073741826], a) + end - class << o1; self; end.class_eval do - define_method(:succ) { o2 } - end + def test_step_non_numeric_range + # finite a = [] - (o1..o2).step(1) {|x| a << x } - assert_equal([o1, o2], a) + ('a'..'aaaa').step('a') { a << _1 } + assert_equal(%w[a aa aaa aaaa], a) + + assert_kind_of(Enumerator, ('a'..'aaaa').step('a')) + assert_equal(%w[a aa aaa aaaa], ('a'..'aaaa').step('a').to_a) a = [] - (o1...o2).step(1) {|x| a << x } - assert_equal([o1], a) + ('a'...'aaaa').step('a') { a << _1 } + assert_equal(%w[a aa aaa], a) - assert_nothing_raised("[ruby-dev:34557]") { (0..2).step(0.5) {|x| } } + assert_kind_of(Enumerator, ('a'...'aaaa').step('a')) + assert_equal(%w[a aa aaa], ('a'...'aaaa').step('a').to_a) + # endless a = [] - (0..2).step(0.5) {|x| a << x } - assert_equal([0, 0.5, 1.0, 1.5, 2.0], a) + ('a'...).step('a') { a << _1; break if a.size == 3 } + assert_equal(%w[a aa aaa], a) + + assert_kind_of(Enumerator, ('a'...).step('a')) + assert_equal(%w[a aa aaa], ('a'...).step('a').take(3)) + + # beginless + assert_raise(ArgumentError) { (...'aaa').step('a') {} } + assert_raise(ArgumentError) { (...'aaa').step('a') } + + # step is not provided + assert_raise(ArgumentError) { (Time.new(2022)...Time.new(2023)).step } + # step is incompatible + assert_raise(TypeError) { (Time.new(2022)...Time.new(2023)).step('a') {} } + assert_raise(TypeError) { (Time.new(2022)...Time.new(2023)).step('a').to_a } + + # step is compatible, but shouldn't convert into numeric domain: a = [] - (0..).step(0.5) {|x| a << x; break if a.size == 5 } - assert_equal([0, 0.5, 1.0, 1.5, 2.0], a) + (Time.utc(2022, 2, 24)...).step(1) { a << _1; break if a.size == 2 } + assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 24, 0, 0, 1)], a) a = [] - (0x40000000..0x40000002).step(0.5) {|x| a << x } - assert_equal([1073741824, 1073741824.5, 1073741825.0, 1073741825.5, 1073741826], a) + (Time.utc(2022, 2, 24)...).step(1.0) { a << _1; break if a.size == 2 } + assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 24, 0, 0, 1)], a) - o = Object.new - def o.to_int() 1 end - assert_nothing_raised("[ruby-dev:34558]") { (0..2).step(o) {|x| } } + a = [] + (Time.utc(2022, 2, 24)...).step(1r) { a << _1; break if a.size == 2 } + assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 24, 0, 0, 1)], a) - o = Object.new - class << o - def to_str() "a" end - def <=>(other) to_str <=> other end - end + # step decreases the value + a = [] + (Time.utc(2022, 2, 24)...).step(-1) { a << _1; break if a.size == 2 } + assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 23, 23, 59, 59)], a) a = [] - (o.."c").step(1) {|x| a << x} - assert_equal(["a", "b", "c"], a) + (Time.utc(2022, 2, 24)...Time.utc(2022, 2, 23, 23, 59, 57)).step(-1) { a << _1 } + assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 23, 23, 59, 59), + Time.utc(2022, 2, 23, 23, 59, 58)], a) + a = [] - (o..).step(1) {|x| a << x; break if a.size >= 3} - assert_equal(["a", "b", "c"], a) + (Time.utc(2022, 2, 24)..Time.utc(2022, 2, 23, 23, 59, 57)).step(-1) { a << _1 } + assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 23, 23, 59, 59), + Time.utc(2022, 2, 23, 23, 59, 58), Time.utc(2022, 2, 23, 23, 59, 57)], a) + + # step decreases, but the range is forward-directed: + a = [] + (Time.utc(2022, 2, 24)...Time.utc(2022, 2, 24, 01, 01, 03)).step(-1) { a << _1 } + assert_equal([], a) + end + + def test_step_string_legacy + # finite + a = [] + ('a'..'g').step(2) { a << _1 } + assert_equal(%w[a c e g], a) + + assert_kind_of(Enumerator, ('a'..'g').step(2)) + assert_equal(%w[a c e g], ('a'..'g').step(2).to_a) + + a = [] + ('a'...'g').step(2) { a << _1 } + assert_equal(%w[a c e], a) + + assert_kind_of(Enumerator, ('a'...'g').step(2)) + assert_equal(%w[a c e], ('a'...'g').step(2).to_a) + + # endless + a = [] + ('a'...).step(2) { a << _1; break if a.size == 3 } + assert_equal(%w[a c e], a) + + assert_kind_of(Enumerator, ('a'...).step(2)) + assert_equal(%w[a c e], ('a'...).step(2).take(3)) + + # beginless + assert_raise(ArgumentError) { (...'g').step(2) {} } + assert_raise(ArgumentError) { (...'g').step(2) } + + # step is not provided + a = [] + ('a'..'d').step { a << _1 } + assert_equal(%w[a b c d], a) + + assert_kind_of(Enumerator, ('a'..'d').step) + assert_equal(%w[a b c d], ('a'..'d').step.to_a) + + a = [] + ('a'...'d').step { a << _1 } + assert_equal(%w[a b c], a) + + assert_kind_of(Enumerator, ('a'...'d').step) + assert_equal(%w[a b c], ('a'...'d').step.to_a) + + # endless + a = [] + ('a'...).step { a << _1; break if a.size == 3 } + assert_equal(%w[a b c], a) + + assert_kind_of(Enumerator, ('a'...).step) + assert_equal(%w[a b c], ('a'...).step.take(3)) + end + + def test_step_symbol_legacy + # finite + a = [] + (:a..:g).step(2) { a << _1 } + assert_equal(%i[a c e g], a) + + assert_kind_of(Enumerator, (:a..:g).step(2)) + assert_equal(%i[a c e g], (:a..:g).step(2).to_a) + + a = [] + (:a...:g).step(2) { a << _1 } + assert_equal(%i[a c e], a) + + assert_kind_of(Enumerator, (:a...:g).step(2)) + assert_equal(%i[a c e], (:a...:g).step(2).to_a) + + # endless + a = [] + (:a...).step(2) { a << _1; break if a.size == 3 } + assert_equal(%i[a c e], a) + + assert_kind_of(Enumerator, (:a...).step(2)) + assert_equal(%i[a c e], (:a...).step(2).take(3)) + + # beginless + assert_raise(ArgumentError) { (...:g).step(2) {} } + assert_raise(ArgumentError) { (...:g).step(2) } + + # step is not provided + a = [] + (:a..:d).step { a << _1 } + assert_equal(%i[a b c d], a) + + assert_kind_of(Enumerator, (:a..:d).step) + assert_equal(%i[a b c d], (:a..:d).step.to_a) + + a = [] + (:a...:d).step { a << _1 } + assert_equal(%i[a b c], a) + + assert_kind_of(Enumerator, (:a...:d).step) + assert_equal(%i[a b c], (:a...:d).step.to_a) + + # endless + a = [] + (:a...).step { a << _1; break if a.size == 3 } + assert_equal(%i[a b c], a) + + assert_kind_of(Enumerator, (:a...).step) + assert_equal(%i[a b c], (:a...).step.take(3)) end def test_step_bug15537 @@ -392,24 +597,20 @@ class TestRange < Test::Unit::TestCase assert_equal(4, (1.0...5.6).step(1.5).to_a.size) end - def test_step_with_succ - c = Struct.new(:i) do - def succ; self.class.new(i+1); end - def <=>(other) i <=> other.i;end - end.new(0) - - result = [] - (c..c.succ).step(2) do |d| - result << d.i + def test_step_with_nonnumeric_endpoint + num = Data.define(:value) do + def coerce(o); [o, 100]; end + def <=>(o) value<=>o; end + def +(o) with(value: value + o) end end - assert_equal([0], result) - - result = [] - (c..).step(2) do |d| - result << d.i - break if d.i >= 4 - end - assert_equal([0, 2, 4], result) + i = num.new(100) + + assert_equal([100], (100..100).step(10).to_a) + assert_equal([], (100...100).step(10).to_a) + assert_equal([100], (100..i).step(10).to_a) + assert_equal([i], (i..100).step(10).to_a) + assert_equal([], (100...i).step(10).to_a) + assert_equal([], (i...100).step(10).to_a) end def test_each @@ -496,6 +697,170 @@ class TestRange < Test::Unit::TestCase assert_equal([0, 1, 2, 3, 4], result) end + def test_reverse_each + a = [] + (1..3).reverse_each {|x| a << x } + assert_equal([3, 2, 1], a) + + a = [] + (1...3).reverse_each {|x| a << x } + assert_equal([2, 1], a) + + fmax = RbConfig::LIMITS['FIXNUM_MAX'] + fmin = RbConfig::LIMITS['FIXNUM_MIN'] + + a = [] + (fmax+1..fmax+3).reverse_each {|x| a << x } + assert_equal([fmax+3, fmax+2, fmax+1], a) + + a = [] + (fmax+1...fmax+3).reverse_each {|x| a << x } + assert_equal([fmax+2, fmax+1], a) + + a = [] + (fmax-1..fmax+1).reverse_each {|x| a << x } + assert_equal([fmax+1, fmax, fmax-1], a) + + a = [] + (fmax-1...fmax+1).reverse_each {|x| a << x } + assert_equal([fmax, fmax-1], a) + + a = [] + (fmin-1..fmin+1).reverse_each{|x| a << x } + assert_equal([fmin+1, fmin, fmin-1], a) + + a = [] + (fmin-1...fmin+1).reverse_each{|x| a << x } + assert_equal([fmin, fmin-1], a) + + a = [] + (fmin-3..fmin-1).reverse_each{|x| a << x } + assert_equal([fmin-1, fmin-2, fmin-3], a) + + a = [] + (fmin-3...fmin-1).reverse_each{|x| a << x } + assert_equal([fmin-2, fmin-3], a) + + a = [] + ("a".."c").reverse_each {|x| a << x } + assert_equal(["c", "b", "a"], a) + end + + def test_reverse_each_for_beginless_range + fmax = RbConfig::LIMITS['FIXNUM_MAX'] + fmin = RbConfig::LIMITS['FIXNUM_MIN'] + + a = [] + (..3).reverse_each {|x| a << x; break if x <= 0 } + assert_equal([3, 2, 1, 0], a) + + a = [] + (...3).reverse_each {|x| a << x; break if x <= 0 } + assert_equal([2, 1, 0], a) + + a = [] + (..fmax+1).reverse_each {|x| a << x; break if x <= fmax-1 } + assert_equal([fmax+1, fmax, fmax-1], a) + + a = [] + (...fmax+1).reverse_each {|x| a << x; break if x <= fmax-1 } + assert_equal([fmax, fmax-1], a) + + a = [] + (..fmin+1).reverse_each {|x| a << x; break if x <= fmin-1 } + assert_equal([fmin+1, fmin, fmin-1], a) + + a = [] + (...fmin+1).reverse_each {|x| a << x; break if x <= fmin-1 } + assert_equal([fmin, fmin-1], a) + + a = [] + (..fmin-1).reverse_each {|x| a << x; break if x <= fmin-3 } + assert_equal([fmin-1, fmin-2, fmin-3], a) + + a = [] + (...fmin-1).reverse_each {|x| a << x; break if x <= fmin-3 } + assert_equal([fmin-2, fmin-3], a) + end + + def test_reverse_each_for_endless_range + assert_raise(TypeError) { (1..).reverse_each {} } + + enum = nil + assert_nothing_raised { enum = (1..).reverse_each } + assert_raise(TypeError) { enum.each {} } + end + + def test_reverse_each_for_single_point_range + fmin = RbConfig::LIMITS['FIXNUM_MIN'] + fmax = RbConfig::LIMITS['FIXNUM_MAX'] + + values = [fmin*2, fmin-1, fmin, 0, fmax, fmax+1, fmax*2] + + values.each do |b| + r = b..b + a = [] + r.reverse_each {|x| a << x } + assert_equal([b], a, "failed on #{r}") + + r = b...b+1 + a = [] + r.reverse_each {|x| a << x } + assert_equal([b], a, "failed on #{r}") + end + end + + def test_reverse_each_for_empty_range + fmin = RbConfig::LIMITS['FIXNUM_MIN'] + fmax = RbConfig::LIMITS['FIXNUM_MAX'] + + values = [fmin*2, fmin-1, fmin, 0, fmax, fmax+1, fmax*2] + + values.each do |b| + r = b..b-1 + a = [] + r.reverse_each {|x| a << x } + assert_equal([], a, "failed on #{r}") + end + + values.repeated_permutation(2).to_a.product([true, false]).each do |(b, e), excl| + next unless b > e || (b == e && excl) + + r = Range.new(b, e, excl) + a = [] + r.reverse_each {|x| a << x } + assert_equal([], a, "failed on #{r}") + end + end + + def test_reverse_each_with_no_block + enum = (1..5).reverse_each + assert_equal 5, enum.size + + a = [] + enum.each {|x| a << x } + assert_equal [5, 4, 3, 2, 1], a + end + + def test_reverse_each_size + assert_equal(3, (1..3).reverse_each.size) + assert_equal(3, (1..3.3).reverse_each.size) + assert_raise(TypeError) { (1..nil).reverse_each.size } + assert_raise(TypeError) { (1.1..3).reverse_each.size } + assert_raise(TypeError) { (1.1..3.3).reverse_each.size } + assert_raise(TypeError) { (1.1..nil).reverse_each.size } + assert_equal(Float::INFINITY, (..3).reverse_each.size) + assert_raise(TypeError) { (nil..3.3).reverse_each.size } + assert_raise(TypeError) { (nil..nil).reverse_each.size } + + assert_equal(2, (1...3).reverse_each.size) + assert_equal(3, (1...3.3).reverse_each.size) + + assert_equal(nil, ('a'..'z').reverse_each.size) + assert_raise(TypeError) { ('a'..).reverse_each.size } + assert_raise(TypeError) { (..'z').reverse_each.size } + end + def test_begin_end assert_equal(0, (0..1).begin) assert_equal(1, (0..1).end) @@ -508,16 +873,20 @@ class TestRange < Test::Unit::TestCase def test_first_last assert_equal([0, 1, 2], (0..10).first(3)) assert_equal([8, 9, 10], (0..10).last(3)) + assert_equal([8, 9, 10], (nil..10).last(3)) assert_equal(0, (0..10).first) assert_equal(10, (0..10).last) + assert_equal(10, (nil..10).last) assert_equal("a", ("a".."c").first) assert_equal("c", ("a".."c").last) assert_equal(0, (2..0).last) assert_equal([0, 1, 2], (0...10).first(3)) assert_equal([7, 8, 9], (0...10).last(3)) + assert_equal([7, 8, 9], (nil...10).last(3)) assert_equal(0, (0...10).first) assert_equal(10, (0...10).last) + assert_equal(10, (nil...10).last) assert_equal("a", ("a"..."c").first) assert_equal("c", ("a"..."c").last) assert_equal(0, (2...0).last) @@ -579,6 +948,10 @@ class TestRange < Test::Unit::TestCase assert_not_operator(0..10, :===, 11) assert_operator(5..nil, :===, 11) assert_not_operator(5..nil, :===, 0) + assert_operator(nil..10, :===, 0) + assert_operator(nil..nil, :===, 0) + assert_operator(nil..nil, :===, Object.new) + assert_not_operator(0..10, :===, 0..10) end def test_eqq_string @@ -586,7 +959,7 @@ class TestRange < Test::Unit::TestCase assert_not_operator('A'..'Z', :===, 'ana') assert_operator('A'.., :===, 'ANA') assert_operator(..'Z', :===, 'ANA') - assert_raise(TypeError) {(nil..nil) === 'ANA'} + assert_operator(nil..nil, :===, 'ANA') end def test_eqq_time @@ -623,6 +996,28 @@ class TestRange < Test::Unit::TestCase assert_operator(c.new(0)..c.new(10), :===, c.new(5), bug12003) end + def test_eqq_unbounded_ruby_bug_19864 + t1 = Date.today + t2 = t1 + 1 + assert_equal(true, (..t1) === t1) + assert_equal(false, (..t1) === t2) + assert_equal(true, (..t2) === t1) + assert_equal(true, (..t2) === t2) + assert_equal(false, (...t1) === t1) + assert_equal(false, (...t1) === t2) + assert_equal(true, (...t2) === t1) + assert_equal(false, (...t2) === t2) + + assert_equal(true, (t1..) === t1) + assert_equal(true, (t1..) === t2) + assert_equal(false, (t2..) === t1) + assert_equal(true, (t2..) === t2) + assert_equal(true, (t1...) === t1) + assert_equal(true, (t1...) === t2) + assert_equal(false, (t2...) === t1) + assert_equal(true, (t2...) === t2) + end + def test_eqq_non_iteratable k = Class.new do include Comparable @@ -812,21 +1207,38 @@ class TestRange < Test::Unit::TestCase end def test_size - assert_equal 42, (1..42).size - assert_equal 41, (1...42).size - assert_equal 6, (1...6.3).size - assert_equal 5, (1.1...6).size - assert_equal 42, (1..42).each.size + Enumerator.product([:to_i, :to_f, :to_r].repeated_permutation(2), [1, 10], [5, 5.5], [true, false]) do |(m1, m2), beg, ende, exclude_end| + r = Range.new(beg.send(m1), ende.send(m2), exclude_end) + iterable = true + yielded = [] + begin + r.each { yielded << _1 } + rescue TypeError + iterable = false + end + + if iterable + assert_equal(yielded.size, r.size, "failed on #{r}") + assert_equal(yielded.size, r.each.size, "failed on #{r}") + else + assert_raise(TypeError, "failed on #{r}") { r.size } + assert_raise(TypeError, "failed on #{r}") { r.each.size } + end + end + assert_nil ("a"..."z").size - assert_nil ("a"...).size - assert_nil (..."z").size # [Bug #18983] - assert_nil (nil...nil).size # [Bug #18983] - assert_equal Float::INFINITY, (1...).size - assert_equal Float::INFINITY, (1.0...).size - assert_equal Float::INFINITY, (...1).size - assert_equal Float::INFINITY, (...1.0).size - assert_nil ("a"...).size + assert_equal Float::INFINITY, (1..).size + assert_raise(TypeError) { (1.0..).size } + assert_raise(TypeError) { (1r..).size } + assert_nil ("a"..).size + + assert_raise(TypeError) { (..1).size } + assert_raise(TypeError) { (..1.0).size } + assert_raise(TypeError) { (..1r).size } + assert_raise(TypeError) { (..'z').size } + + assert_raise(TypeError) { (nil...nil).size } end def test_bsearch_typechecks_return_values @@ -850,9 +1262,6 @@ class TestRange < Test::Unit::TestCase assert_raise(TypeError) { (Rational(-1,2)..Rational(9,4)).bsearch } - assert_raise(TypeError) { - (BigDecimal('0.5')..BigDecimal('2.25')).bsearch - } end def test_bsearch_for_fixnum @@ -1026,7 +1435,10 @@ class TestRange < Test::Unit::TestCase assert_equal(nil, (bignum...bignum+ary.size).bsearch {|i| ary[i - bignum] >= 100 }) assert_equal(bignum + 0, (bignum...bignum+ary.size).bsearch {|i| true }) assert_equal(nil, (bignum...bignum+ary.size).bsearch {|i| false }) + + assert_equal(bignum * 2 + 1, (0...).bsearch {|i| i > bignum * 2 }) assert_equal(bignum * 2 + 1, (bignum...).bsearch {|i| i > bignum * 2 }) + assert_equal(-bignum * 2 + 1, (...0).bsearch {|i| i > -bignum * 2 }) assert_equal(-bignum * 2 + 1, (...-bignum).bsearch {|i| i > -bignum * 2 }) assert_raise(TypeError) { ("a".."z").bsearch {} } @@ -1046,11 +1458,94 @@ 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 def test_count + assert_equal 42, (1..42).count + assert_equal 41, (1...42).count + assert_equal 0, (42..1).count + assert_equal 0, (42...1).count + assert_equal 2**100, (1..2**100).count + assert_equal 6, (1...6.3).count + assert_equal 4, ('a'..'d').count + assert_equal 3, ('a'...'d').count + assert_equal(Float::INFINITY, (1..).count) + assert_equal(Float::INFINITY, (..1).count) + end + + def test_overlap? + assert_not_operator(0..2, :overlap?, -2..-1) + assert_not_operator(0..2, :overlap?, -2...0) + assert_operator(0..2, :overlap?, -1..0) + assert_operator(0..2, :overlap?, 1..2) + assert_operator(0..2, :overlap?, 2..3) + assert_not_operator(0..2, :overlap?, 3..4) + assert_not_operator(0...2, :overlap?, 2..3) + + assert_operator(..0, :overlap?, -1..0) + assert_operator(...0, :overlap?, -1..0) + assert_operator(..0, :overlap?, 0..1) + assert_operator(..0, :overlap?, ..1) + assert_not_operator(..0, :overlap?, 1..2) + assert_not_operator(...0, :overlap?, 0..1) + + assert_not_operator(0.., :overlap?, -2..-1) + assert_not_operator(0.., :overlap?, ...0) + assert_operator(0.., :overlap?, -1..0) + assert_operator(0.., :overlap?, ..0) + assert_operator(0.., :overlap?, 0..1) + assert_operator(0.., :overlap?, 1..2) + assert_operator(0.., :overlap?, 1..) + + assert_not_operator((1..3), :overlap?, ('a'..'d')) + assert_not_operator((1..), :overlap?, ('a'..)) + assert_not_operator((..1), :overlap?, (..'a')) + + assert_raise(TypeError) { (0..).overlap?(1) } + assert_raise(TypeError) { (0..).overlap?(nil) } + + assert_operator((1..3), :overlap?, (2..4)) + assert_operator((1...3), :overlap?, (2..3)) + assert_operator((2..3), :overlap?, (1..2)) + assert_operator((..3), :overlap?, (3..)) + assert_operator((nil..nil), :overlap?, (3..)) + assert_operator((nil...nil), :overlap?, (nil..)) + assert_operator((nil..nil), :overlap?, (..3)) + assert_operator((..3), :overlap?, (nil..nil)) + + assert_raise(TypeError) { (1..3).overlap?(1) } + + assert_not_operator((1..2), :overlap?, (2...2)) + assert_not_operator((2...2), :overlap?, (1..2)) + + assert_not_operator((4..1), :overlap?, (2..3)) + assert_not_operator((4..1), :overlap?, (..3)) + assert_not_operator((4..1), :overlap?, (2..)) + + assert_not_operator((1..4), :overlap?, (3..2)) + assert_not_operator((..4), :overlap?, (3..2)) + assert_not_operator((1..), :overlap?, (3..2)) + + assert_not_operator((4..5), :overlap?, (2..3)) + assert_not_operator((4..5), :overlap?, (2...4)) + + assert_not_operator((1..2), :overlap?, (3..4)) + assert_not_operator((1...3), :overlap?, (3..4)) + + assert_not_operator((4..5), :overlap?, (2..3)) + assert_not_operator((4..5), :overlap?, (2...4)) + + assert_not_operator((1..2), :overlap?, (3..4)) + assert_not_operator((1...3), :overlap?, (3..4)) + assert_not_operator((...3), :overlap?, (3..)) end end diff --git a/test/ruby/test_rational.rb b/test/ruby/test_rational.rb index a51ce3dc99..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?) } @@ -1066,11 +1070,10 @@ class Rational_Test < Test::Unit::TestCase end def test_power_overflow - bug = '[ruby-core:79686] [Bug #13242]: Infinity due to overflow' - x = EnvUtil.suppress_warning {4r**40000000} - assert_predicate x, :infinite?, bug - x = EnvUtil.suppress_warning {(1/4r)**40000000} - assert_equal 0, x, bug + assert_raise(ArgumentError) { 4r**400000000000000000000 } + exp = 4**40000000 + assert_equal exp, 4r**40000000 + assert_equal 1r/exp, (1/4r)**40000000 end def test_positive_p diff --git a/test/ruby/test_refinement.rb b/test/ruby/test_refinement.rb index 56f33ae00a..209e55294b 100644 --- a/test/ruby/test_refinement.rb +++ b/test/ruby/test_refinement.rb @@ -825,7 +825,7 @@ class TestRefinement < Test::Unit::TestCase GC.stress = true 10.times{ #{PrependAfterRefine_CODE} - undef PrependAfterRefine + Object.send(:remove_const, :PrependAfterRefine) } }, timeout: 60 end @@ -1044,7 +1044,7 @@ class TestRefinement < Test::Unit::TestCase end using Test def t - 'Refinements are broken!'.chop! + 'Refinements are broken!'.dup.chop! end t module Test @@ -1606,18 +1606,35 @@ class TestRefinement < Test::Unit::TestCase end using R + def m + C.new.m + end + assert_equal(:foo, C.new.m) + assert_equal(:foo, m) module R refine C do + + assert_equal(:foo, C.new.m) + assert_equal(:foo, m) + alias m m + + assert_equal(:foo, C.new.m) + assert_equal(:foo, m) + def m :bar end + + assert_equal(:bar, C.new.m, "[ruby-core:71423] [Bug #11672]") + assert_equal(:bar, m, "[Bug #20285]") end end assert_equal(:bar, C.new.m, "[ruby-core:71423] [Bug #11672]") + assert_equal(:bar, m, "[Bug #20285]") end; end @@ -1798,7 +1815,7 @@ class TestRefinement < Test::Unit::TestCase assert_equal([int_refinement, str_refinement], m.refinements) end - def test_refined_class + def test_target refinements = Module.new { refine Integer do end @@ -1806,8 +1823,8 @@ class TestRefinement < Test::Unit::TestCase refine String do end }.refinements - assert_equal(Integer, refinements[0].refined_class) - assert_equal(String, refinements[1].refined_class) + assert_equal(Integer, refinements[0].target) + assert_equal(String, refinements[1].target) end def test_warn_setconst_in_refinmenet @@ -1916,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 @@ -2626,6 +2666,99 @@ class TestRefinement < Test::Unit::TestCase assert_equal([], Refinement.used_modules) end + def test_inlinecache + assert_separately([], <<-"end;") + module R + refine String do + def to_s = :R + end + end + + 2.times{|i| + s = ''.to_s + assert_equal '', s if i == 0 + assert_equal :R, s if i == 1 + using R if i == 0 + assert_equal :R, ''.to_s + } + end; + end + + def test_inline_cache_invalidation + klass = Class.new do + def cached_foo_callsite = foo + + def foo = :v1 + + host = self + @refinement = Module.new do + refine(host) do + def foo = :unused + end + end + end + + obj = klass.new + obj.cached_foo_callsite # prime cache + klass.class_eval do + def foo = :v2 # invalidate + end + assert_equal(:v2, obj.cached_foo_callsite) + end + + # [Bug #20302] + def test_multiple_refinements_for_same_module + assert_in_out_err([], <<-INPUT, %w(:f2 :f1), []) + module M1 + refine(Kernel) do + def f1 = :f1 + end + end + + module M2 + refine(Kernel) do + def f2 = :f2 + end + end + + class Foo + using M1 + using M2 + + def test + p f2 + p f1 + end + end + + Foo.new.test + 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 81dac20648..9feababa53 100644 --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -72,6 +72,19 @@ class TestRegexp < Test::Unit::TestCase end end + def test_to_s_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + str = "abcd\u3042" + [:UTF_16BE, :UTF_16LE, :UTF_32BE, :UTF_32LE].each do |es| + enc = Encoding.const_get(es) + rs = Regexp.new(str.encode(enc)).to_s + assert_equal("(?-mix:abcd\u3042)".encode(enc), rs) + assert_equal(enc, rs.encoding) + end + end + end + def test_to_s_extended_subexp re = /#\g#{"\n"}/x re = /#{re}/ @@ -187,6 +200,13 @@ class TestRegexp < Test::Unit::TestCase RUBY end + def test_utf8_comment_in_usascii_extended_regexp_bug_19455 + assert_separately([], <<-RUBY) + assert_equal(Encoding::UTF_8, /(?#\u1000)/x.encoding) + assert_equal(Encoding::UTF_8, /#\u1000/x.encoding) + RUBY + end + def test_union assert_equal :ok, begin Regexp.union( @@ -304,6 +324,9 @@ class TestRegexp < Test::Unit::TestCase assert_equal({'a' => '1', 'b' => '2', 'c' => '3'}, /^(?<a>.)(?<b>.)(?<c>.)?/.match('123').named_captures) assert_equal({'a' => '1', 'b' => '2', 'c' => ''}, /^(?<a>.)(?<b>.)(?<c>.?)/.match('12').named_captures) + assert_equal({a: '1', b: '2', c: ''}, /^(?<a>.)(?<b>.)(?<c>.?)/.match('12').named_captures(symbolize_names: true)) + assert_equal({'a' => '1', 'b' => '2', 'c' => ''}, /^(?<a>.)(?<b>.)(?<c>.?)/.match('12').named_captures(symbolize_names: false)) + assert_equal({'a' => 'x'}, /(?<a>x)|(?<a>y)/.match('x').named_captures) assert_equal({'a' => 'y'}, /(?<a>x)|(?<a>y)/.match('y').named_captures) @@ -447,6 +470,13 @@ class TestRegexp < Test::Unit::TestCase assert_equal('/\/\xF1\xF2\xF3/i', /\/#{s}/i.inspect) end + def test_inspect_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + assert_equal('/(?-mix:\\/)|/', Regexp.union(/\//, "").inspect) + end + end + def test_char_to_option assert_equal("BAR", "FOOBARBAZ"[/b../i]) assert_equal("bar", "foobarbaz"[/ b . . /x]) @@ -529,16 +559,26 @@ class TestRegexp < Test::Unit::TestCase assert_raise(IndexError) { m.byteoffset(2) } assert_raise(IndexError) { m.begin(2) } assert_raise(IndexError) { m.end(2) } + assert_raise(IndexError) { m.bytebegin(2) } + assert_raise(IndexError) { m.byteend(2) } m = /(?<x>q..)?/.match("foobarbaz") assert_equal([nil, nil], m.byteoffset("x")) assert_equal(nil, m.begin("x")) assert_equal(nil, m.end("x")) + assert_equal(nil, m.bytebegin("x")) + assert_equal(nil, m.byteend("x")) m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044") assert_equal([3, 6], m.byteoffset(1)) + assert_equal(3, m.bytebegin(1)) + assert_equal(6, m.byteend(1)) assert_equal([nil, nil], m.byteoffset(2)) + assert_equal(nil, m.bytebegin(2)) + assert_equal(nil, m.byteend(2)) assert_equal([6, 9], m.byteoffset(3)) + assert_equal(6, m.bytebegin(3)) + assert_equal(9, m.byteend(3)) end def test_match_to_s @@ -683,6 +723,18 @@ class TestRegexp < Test::Unit::TestCase } end + def test_match_no_match_no_matchdata + EnvUtil.without_gc do + h = {} + ObjectSpace.count_objects(h) + prev_matches = h[:T_MATCH] || 0 + _md = /[A-Z]/.match('1') # no match + ObjectSpace.count_objects(h) + new_matches = h[:T_MATCH] || 0 + assert_equal prev_matches, new_matches, "Bug [#20104]" + end + end + def test_initialize assert_raise(ArgumentError) { Regexp.new } assert_equal(/foo/, assert_warning(/ignored/) {Regexp.new(/foo/, Regexp::IGNORECASE)}) @@ -713,6 +765,12 @@ class TestRegexp < Test::Unit::TestCase assert_raise(RegexpError) { Regexp.new("((?<v>))\\g<0>") } end + def test_initialize_from_regex_memory_corruption + assert_ruby_status([], <<-'end;') + 10_000.times { Regexp.new(Regexp.new("(?<name>)")) } + end; + end + def test_initialize_bool_warning assert_warning(/expected true or false as ignorecase/) do Regexp.new("foo", :i) @@ -844,6 +902,14 @@ class TestRegexp < Test::Unit::TestCase $_ = nil; assert_nil(~/./) end + def test_match_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + m = /(?<foo>.)(?<n>[^aeiou])?(?<bar>.+)/.match("hoge\u3042") + assert_equal("h", m.match(:foo)) + end + end + def test_match_p /backref/ =~ 'backref' # must match here, but not in a separate method, e.g., assert_send, @@ -933,6 +999,18 @@ class TestRegexp < Test::Unit::TestCase assert_equal('foobazquux/foobazquux', result, bug8856) end + def test_regsub_no_memory_leak + assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~"end;"}", rss: true) + code = proc do + "aaaaaaaaaaa".gsub(/a/, "") + end + + 1_000.times(&code) + begin; + 100_000.times(&code) + end; + end + def test_ignorecase v = assert_deprecated_warning(/variable \$= is no longer effective/) { $= } assert_equal(false, v) @@ -958,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) } @@ -1230,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 @@ -1350,30 +1433,223 @@ class TestRegexp < Test::Unit::TestCase end def test_unicode_age - assert_match(/^\p{Age=6.0}$/u, "\u261c") - assert_match(/^\p{Age=1.1}$/u, "\u261c") - assert_no_match(/^\P{age=6.0}$/u, "\u261c") - - assert_match(/^\p{age=6.0}$/u, "\u31f6") - assert_match(/^\p{age=3.2}$/u, "\u31f6") - assert_no_match(/^\p{age=3.1}$/u, "\u31f6") - assert_no_match(/^\p{age=3.0}$/u, "\u31f6") - assert_no_match(/^\p{age=1.1}$/u, "\u31f6") - - assert_match(/^\p{age=6.0}$/u, "\u2754") - assert_no_match(/^\p{age=5.0}$/u, "\u2754") - assert_no_match(/^\p{age=4.0}$/u, "\u2754") - assert_no_match(/^\p{age=3.0}$/u, "\u2754") - assert_no_match(/^\p{age=2.0}$/u, "\u2754") - assert_no_match(/^\p{age=1.1}$/u, "\u2754") - - assert_no_match(/^\p{age=12.0}$/u, "\u32FF") - assert_match(/^\p{age=12.1}$/u, "\u32FF") - assert_no_match(/^\p{age=13.0}$/u, "\u{10570}") - assert_match(/^\p{age=14.0}$/u, "\u{10570}") - assert_match(/^\p{age=14.0}$/u, "\u9FFF") - assert_match(/^\p{age=14.0}$/u, "\u{2A6DF}") - assert_match(/^\p{age=14.0}$/u, "\u{2B738}") + assert_unicode_age("\u261c", matches: %w"6.0 1.1", unmatches: []) + + assert_unicode_age("\u31f6", matches: %w"6.0 3.2", unmatches: %w"3.1 3.0 1.1") + assert_unicode_age("\u2754", matches: %w"6.0", unmatches: %w"5.0 4.0 3.0 2.0 1.1") + + assert_unicode_age("\u32FF", matches: %w"12.1", unmatches: %w"12.0") + end + + def test_unicode_age_14_0 + @matches = %w"14.0" + @unmatches = %w"13.0" + + assert_unicode_age("\u{10570}") + assert_unicode_age("\u9FFF") + assert_unicode_age("\u{2A6DF}") + assert_unicode_age("\u{2B738}") + end + + def test_unicode_age_15_0 + @matches = %w"15.0" + @unmatches = %w"14.0" + + assert_unicode_age("\u{0CF3}", + "KANNADA SIGN COMBINING ANUSVARA ABOVE RIGHT") + assert_unicode_age("\u{0ECE}", "LAO YAMAKKAN") + assert_unicode_age("\u{10EFD}".."\u{10EFF}", + "ARABIC SMALL LOW WORD SAKTA..ARABIC SMALL LOW WORD MADDA") + assert_unicode_age("\u{1123F}".."\u{11241}", + "KHOJKI LETTER QA..KHOJKI VOWEL SIGN VOCALIC R") + assert_unicode_age("\u{11B00}".."\u{11B09}", + "DEVANAGARI HEAD MARK..DEVANAGARI SIGN MINDU") + assert_unicode_age("\u{11F00}".."\u{11F10}", + "KAWI SIGN CANDRABINDU..KAWI LETTER O") + assert_unicode_age("\u{11F12}".."\u{11F3A}", + "KAWI LETTER KA..KAWI VOWEL SIGN VOCALIC R") + assert_unicode_age("\u{11F3E}".."\u{11F59}", + "KAWI VOWEL SIGN E..KAWI DIGIT NINE") + assert_unicode_age("\u{1342F}", + "EGYPTIAN HIEROGLYPH V011D") + assert_unicode_age("\u{13439}".."\u{1343F}", + "EGYPTIAN HIEROGLYPH INSERT AT MIDDLE..EGYPTIAN HIEROGLYPH END WALLED ENCLOSURE") + assert_unicode_age("\u{13440}".."\u{13455}", + "EGYPTIAN HIEROGLYPH MIRROR HORIZONTALLY..EGYPTIAN HIEROGLYPH MODIFIER DAMAGED") + assert_unicode_age("\u{1B132}", "HIRAGANA LETTER SMALL KO") + assert_unicode_age("\u{1B155}", "KATAKANA LETTER SMALL KO") + assert_unicode_age("\u{1D2C0}".."\u{1D2D3}", + "KAKTOVIK NUMERAL ZERO..KAKTOVIK NUMERAL NINETEEN") + assert_unicode_age("\u{1DF25}".."\u{1DF2A}", + "LATIN SMALL LETTER D WITH MID-HEIGHT LEFT HOOK..LATIN SMALL LETTER T WITH MID-HEIGHT LEFT HOOK") + assert_unicode_age("\u{1E030}".."\u{1E06D}", + "MODIFIER LETTER CYRILLIC SMALL A..MODIFIER LETTER CYRILLIC SMALL STRAIGHT U WITH STROKE") + assert_unicode_age("\u{1E08F}", + "COMBINING CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I") + assert_unicode_age("\u{1E4D0}".."\u{1E4F9}", + "NAG MUNDARI LETTER O..NAG MUNDARI DIGIT NINE") + assert_unicode_age("\u{1F6DC}", "WIRELESS") + assert_unicode_age("\u{1F774}".."\u{1F776}", + "LOT OF FORTUNE..LUNAR ECLIPSE") + assert_unicode_age("\u{1F77B}".."\u{1F77F}", + "HAUMEA..ORCUS") + assert_unicode_age("\u{1F7D9}", "NINE POINTED WHITE STAR") + assert_unicode_age("\u{1FA75}".."\u{1FA77}", + "LIGHT BLUE HEART..PINK HEART") + assert_unicode_age("\u{1FA87}".."\u{1FA88}", + "MARACAS..FLUTE") + assert_unicode_age("\u{1FAAD}".."\u{1FAAF}", + "FOLDING HAND FAN..KHANDA") + assert_unicode_age("\u{1FABB}".."\u{1FABD}", + "HYACINTH..WING") + assert_unicode_age("\u{1FABF}", "GOOSE") + assert_unicode_age("\u{1FACE}".."\u{1FACF}", + "MOOSE..DONKEY") + assert_unicode_age("\u{1FADA}".."\u{1FADB}", + "GINGER ROOT..PEA POD") + assert_unicode_age("\u{1FAE8}", "SHAKING FACE") + assert_unicode_age("\u{1FAF7}".."\u{1FAF8}", + "LEFTWARDS PUSHING HAND..RIGHTWARDS PUSHING HAND") + assert_unicode_age("\u{2B739}", + "CJK UNIFIED IDEOGRAPH-2B739") + assert_unicode_age("\u{31350}".."\u{323AF}", + "CJK UNIFIED IDEOGRAPH-31350..CJK UNIFIED IDEOGRAPH-323AF") + end + + def test_unicode_age_15_1 + @matches = %w"15.1" + @unmatches = %w"15.0" + + # https://www.unicode.org/Public/15.1.0/ucd/DerivedAge.txt + assert_unicode_age("\u{2FFC}".."\u{2FFF}", + "IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM RIGHT..IDEOGRAPHIC DESCRIPTION CHARACTER ROTATION") + assert_unicode_age("\u{31EF}", + "IDEOGRAPHIC DESCRIPTION CHARACTER SUBTRACTION") + assert_unicode_age("\u{2EBF0}".."\u{2EE5D}", + "CJK UNIFIED IDEOGRAPH-2EBF0..CJK UNIFIED IDEOGRAPH-2EE5D") + end + + def test_unicode_age_16_0 + @matches = %w"16.0" + @unmatches = %w"15.1" + + # https://www.unicode.org/Public/16.0.0/ucd/DerivedAge.txt + assert_unicode_age("\u{0897}", + "ARABIC PEPET") + assert_unicode_age("\u{1B4E}".."\u{1B4F}", + "BALINESE INVERTED CARIK SIKI..BALINESE INVERTED CARIK PAREREN") + assert_unicode_age("\u{1B7F}", + "BALINESE PANTI BAWAK") + assert_unicode_age("\u{1C89}".."\u{1C8A}", + "CYRILLIC CAPITAL LETTER TJE..CYRILLIC SMALL LETTER TJE") + assert_unicode_age("\u{2427}".."\u{2429}", + "SYMBOL FOR DELETE SQUARE CHECKER BOARD FORM..SYMBOL FOR DELETE MEDIUM SHADE FORM") + assert_unicode_age("\u{31E4}".."\u{31E5}", + "CJK STROKE HXG..CJK STROKE SZP") + assert_unicode_age("\u{A7CB}".."\u{A7CD}", + "LATIN CAPITAL LETTER RAMS HORN..LATIN SMALL LETTER S WITH DIAGONAL STROKE") + assert_unicode_age("\u{A7DA}".."\u{A7DC}", + "LATIN CAPITAL LETTER LAMBDA..LATIN CAPITAL LETTER LAMBDA WITH STROKE") + assert_unicode_age("\u{105C0}".."\u{105F3}", + "TODHRI LETTER A..TODHRI LETTER OO") + assert_unicode_age("\u{10D40}".."\u{10D65}", + "GARAY DIGIT ZERO..GARAY CAPITAL LETTER OLD NA") + assert_unicode_age("\u{10D69}".."\u{10D85}", + "GARAY VOWEL SIGN E..GARAY SMALL LETTER OLD NA") + assert_unicode_age("\u{10D8E}".."\u{10D8F}", + "GARAY PLUS SIGN..GARAY MINUS SIGN") + assert_unicode_age("\u{10EC2}".."\u{10EC4}", + "ARABIC LETTER DAL WITH TWO DOTS VERTICALLY BELOW..ARABIC LETTER KAF WITH TWO DOTS VERTICALLY BELOW") + assert_unicode_age("\u{10EFC}", + "ARABIC COMBINING ALEF OVERLAY") + assert_unicode_age("\u{11380}".."\u{11389}", + "TULU-TIGALARI LETTER A..TULU-TIGALARI LETTER VOCALIC LL") + assert_unicode_age("\u{1138B}", + "TULU-TIGALARI LETTER EE") + assert_unicode_age("\u{1138E}", + "TULU-TIGALARI LETTER AI") + assert_unicode_age("\u{11390}".."\u{113B5}", + "TULU-TIGALARI LETTER OO..TULU-TIGALARI LETTER LLLA") + assert_unicode_age("\u{113B7}".."\u{113C0}", + "TULU-TIGALARI SIGN AVAGRAHA..TULU-TIGALARI VOWEL SIGN VOCALIC LL") + assert_unicode_age("\u{113C2}", + "TULU-TIGALARI VOWEL SIGN EE") + assert_unicode_age("\u{113C5}", + "TULU-TIGALARI VOWEL SIGN AI") + assert_unicode_age("\u{113C7}".."\u{113CA}", + "TULU-TIGALARI VOWEL SIGN OO..TULU-TIGALARI SIGN CANDRA ANUNASIKA") + assert_unicode_age("\u{113CC}".."\u{113D5}", + "TULU-TIGALARI SIGN ANUSVARA..TULU-TIGALARI DOUBLE DANDA") + assert_unicode_age("\u{113D7}".."\u{113D8}", + "TULU-TIGALARI SIGN OM PUSHPIKA..TULU-TIGALARI SIGN SHRII PUSHPIKA") + assert_unicode_age("\u{113E1}".."\u{113E2}", + "TULU-TIGALARI VEDIC TONE SVARITA..TULU-TIGALARI VEDIC TONE ANUDATTA") + assert_unicode_age("\u{116D0}".."\u{116E3}", + "MYANMAR PAO DIGIT ZERO..MYANMAR EASTERN PWO KAREN DIGIT NINE") + assert_unicode_age("\u{11BC0}".."\u{11BE1}", + "SUNUWAR LETTER DEVI..SUNUWAR SIGN PVO") + assert_unicode_age("\u{11BF0}".."\u{11BF9}", + "SUNUWAR DIGIT ZERO..SUNUWAR DIGIT NINE") + assert_unicode_age("\u{11F5A}", + "KAWI SIGN NUKTA") + assert_unicode_age("\u{13460}".."\u{143FA}", + "EGYPTIAN HIEROGLYPH-13460..EGYPTIAN HIEROGLYPH-143FA") + assert_unicode_age("\u{16100}".."\u{16139}", + "GURUNG KHEMA LETTER A..GURUNG KHEMA DIGIT NINE") + assert_unicode_age("\u{16D40}".."\u{16D79}", + "KIRAT RAI SIGN ANUSVARA..KIRAT RAI DIGIT NINE") + assert_unicode_age("\u{18CFF}", + "KHITAN SMALL SCRIPT CHARACTER-18CFF") + assert_unicode_age("\u{1CC00}".."\u{1CCF9}", + "UP-POINTING GO-KART..OUTLINED DIGIT NINE") + assert_unicode_age("\u{1CD00}".."\u{1CEB3}", + "BLOCK OCTANT-3..BLACK RIGHT TRIANGLE CARET") + assert_unicode_age("\u{1E5D0}".."\u{1E5FA}", + "OL ONAL LETTER O..OL ONAL DIGIT NINE") + assert_unicode_age("\u{1E5FF}", + "OL ONAL ABBREVIATION SIGN") + assert_unicode_age("\u{1F8B2}".."\u{1F8BB}", + "RIGHTWARDS ARROW WITH LOWER HOOK..SOUTH WEST ARROW FROM BAR") + assert_unicode_age("\u{1F8C0}".."\u{1F8C1}", + "LEFTWARDS ARROW FROM DOWNWARDS ARROW..RIGHTWARDS ARROW FROM DOWNWARDS ARROW") + assert_unicode_age("\u{1FA89}", + "HARP") + assert_unicode_age("\u{1FA8F}", + "SHOVEL") + assert_unicode_age("\u{1FABE}", + "LEAFLESS TREE") + assert_unicode_age("\u{1FAC6}", + "FINGERPRINT") + assert_unicode_age("\u{1FADC}", + "ROOT VEGETABLE") + assert_unicode_age("\u{1FADF}", + "SPLATTER") + assert_unicode_age("\u{1FAE9}", + "FACE WITH BAGS UNDER EYES") + assert_unicode_age("\u{1FBCB}".."\u{1FBEF}", + "WHITE CROSS MARK..TOP LEFT JUSTIFIED LOWER RIGHT QUARTER BLACK CIRCLE") + end + + UnicodeAgeRegexps = Hash.new do |h, age| + h[age] = [/\A\p{age=#{age}}+\z/u, /\A\P{age=#{age}}+\z/u].freeze + end + + def assert_unicode_age(char, mesg = nil, matches: @matches, unmatches: @unmatches) + if Range === char + char = char.to_a.join("") + end + + matches.each do |age| + pos, neg = UnicodeAgeRegexps[age] + assert_match(pos, char, mesg) + assert_not_match(neg, char, mesg) + end + + unmatches.each do |age| + pos, neg = UnicodeAgeRegexps[age] + assert_not_match(pos, char, mesg) + assert_match(neg, char, mesg) + end end MatchData_A = eval("class MatchData_\u{3042} < MatchData; self; end") @@ -1393,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") } @@ -1467,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]]) @@ -1604,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) @@ -1648,7 +1981,7 @@ class TestRegexp < Test::Unit::TestCase end t = Time.now - t - assert_in_delta(timeout, t, timeout / 2) + assert_operator(timeout, :<=, [timeout * 1.5, 1].max) end; end @@ -1675,8 +2008,40 @@ class TestRegexp < Test::Unit::TestCase end; end + def test_s_timeout_memory_leak + assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~"end;"}", "[Bug #20228]", rss: true) + Regexp.timeout = 0.001 + regex = /^(a*)*$/ + str = "a" * 1000000 + "x" + + code = proc do + regex =~ str + rescue + end + + 10.times(&code) + begin; + 1_000.times(&code) + end; + end + + def test_bug_20453 + re = Regexp.new("^(a*)x$", timeout: 0.001) + + assert_raise(Regexp::TimeoutError) do + re =~ "a" * 1000000 + "x" + end + end + + def test_bug_20886 + re = Regexp.new("d()*+|a*a*bc", timeout: 0.02) + assert_raise(Regexp::TimeoutError) do + re === "b" + "a" * 1000 + end + 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 } @@ -1701,10 +2066,12 @@ class TestRegexp < Test::Unit::TestCase end def test_timeout_shorter_than_global - per_instance_redos_test(10, 0.2, 0.2) + omit "timeout test is too unstable on s390x" if RUBY_PLATFORM =~ /s390x/ + per_instance_redos_test(10, 0.5, 0.5) end def test_timeout_longer_than_global + omit "timeout test is too unstable on s390x" if RUBY_PLATFORM =~ /s390x/ per_instance_redos_test(0.01, 0.5, 0.5) end @@ -1730,32 +2097,126 @@ class TestRegexp < Test::Unit::TestCase end; end - def test_cache_optimization_exponential + def test_timeout_memory_leak + assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~'end;'}", "[Bug #20650]", timeout: 100, rss: true) + regex = Regexp.new("^#{"(a*)" * 10_000}x$", timeout: 0.000001) + str = "a" * 1_000_000 + "x" + + code = proc do + regex =~ str + rescue + end + + 10.times(&code) + begin; + 1_000.times(&code) + end; + end + + def test_match_cache_exponential assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } begin; Regexp.timeout = timeout - assert_nil(/^(a*)*$/ =~ "a" * 1000000 + "x") end; end - def test_cache_optimization_square + def test_match_cache_square assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } begin; Regexp.timeout = timeout - assert_nil(/^a*b?a*$/ =~ "a" * 1000000 + "x") end; end + def test_match_cache_atomic + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_nil(/^a*?(?>a*a*)$/ =~ "a" * 1000000 + "x") + end; + end + + def test_match_cache_atomic_complex + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_nil(/a*(?>a*)ab/ =~ "a" * 1000000 + "b") + end; + end + + def test_match_cache_positive_look_ahead + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}", timeout: 30) + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_nil(/^a*?(?=a*a*)$/ =~ "a" * 1000000 + "x") + end; + end + + def test_match_cache_positive_look_ahead_complex + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}", timeout: 30) + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_equal(/(?:(?=a*)a)*/ =~ "a" * 1000000, 0) + end; + end + + def test_match_cache_negative_look_ahead + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_nil(/^a*?(?!a*a*)$/ =~ "a" * 1000000 + "x") + end; + end + + def test_match_cache_positive_look_behind + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_nil(/(?<=abc|def)(a|a)*$/ =~ "abc" + "a" * 1000000 + "x") + end; + end + + def test_match_cache_negative_look_behind + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_nil(/(?<!x)(a|a)*$/ =~ "a" * 1000000 + "x") + end; + end + + def test_match_cache_with_peek_optimization + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } + begin; + Regexp.timeout = timeout + assert_nil(/a+z/ =~ "a" * 1000000 + "xz") + end; + end + + def test_cache_opcodes_initialize + str = 'test1-test2-test3-test4-test_5' + re = '^([0-9a-zA-Z\-/]*){1,256}$' + 100.times do + assert !Regexp.new(re).match?(str) + end + end + def test_bug_19273 # [Bug #19273] pattern = /(?:(?:-?b)|(?:-?(?:1_?(?:0_?)*)?0))(?::(?:(?:-?b)|(?:-?(?:1_?(?:0_?)*)?0))){0,3}/ assert_equal("10:0:0".match(pattern)[0], "10:0:0") end - def test_bug_19467 + def test_bug_19467 # [Bug #19467] assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") timeout = #{ EnvUtil.apply_timeout_scale(10).inspect } begin; @@ -1770,6 +2231,57 @@ class TestRegexp < Test::Unit::TestCase assert_equal("123456789".match(/(?:x?\dx?){2,}/)[0], "123456789") end + def test_encoding_flags_are_preserved_when_initialized_with_another_regexp + re = Regexp.new("\u2018hello\u2019".encode("UTF-8")) + str = "".encode("US-ASCII") + + assert_nothing_raised do + str.match?(re) + str.match?(Regexp.new(re)) + end + end + + def test_bug_19537 # [Bug #19537] + str = 'aac' + re = '^([ab]{1,3})(a?)*$' + 100.times do + assert !Regexp.new(re).match?(str) + end + end + + def test_bug_20083 # [Bug #20083] + re = /([\s]*ABC)$/i + (1..100).each do |n| + text = "#{"0" * n}ABC" + assert text.match?(re) + end + end + + def test_bug_20098 # [Bug #20098] + assert(/a((.|.)|bc){,4}z/.match? 'abcbcbcbcz') + assert(/a(b+?c*){4,5}z/.match? 'abbbccbbbccbcbcz') + assert(/a(b+?(.|.)){2,3}z/.match? 'abbbcbbbcbbbcz') + assert(/a(b*?(.|.)[bc]){2,5}z/.match? 'abcbbbcbcccbcz') + assert(/^(?:.+){2,4}?b|b/.match? "aaaabaa") + end + + def test_bug_20207 # [Bug #20207] + assert(!'clan'.match?(/(?=.*a)(?!.*n)/)) + end + + def test_bug_20212 # [Bug #20212] + regex = Regexp.new( + /\A((?=.*?[a-z])(?!.*--)[a-z\d]+[a-z\d-]*[a-z\d]+).((?=.*?[a-z])(?!.*--)[a-z\d]+[a-z\d-]*[a-z\d]+).((?=.*?[a-z])(?!.*--)[a-z]+[a-z-]*[a-z]+).((?=.*?[a-z])(?!.*--)[a-z]+[a-z-]*[a-z]+)\Z/x + ) + string = "www.google.com" + 100.times.each { assert(regex.match?(string)) } + end + + def test_bug_20246 # [Bug #20246] + assert_equal '1.2.3', '1.2.3'[/(\d+)(\.\g<1>){2}/] + assert_equal '1.2.3', '1.2.3'[/((?:\d|foo|bar)+)(\.\g<1>){2}/] + end + def test_linear_time_p assert_send [Regexp, :linear_time?, /a/] assert_send [Regexp, :linear_time?, 'a'] @@ -1777,6 +2289,9 @@ class TestRegexp < Test::Unit::TestCase assert_not_send [Regexp, :linear_time?, /(a)\1/] assert_not_send [Regexp, :linear_time?, "(a)\\1"] + assert_not_send [Regexp, :linear_time?, /(?=(a))/] + assert_not_send [Regexp, :linear_time?, /(?!(a))/] + assert_raise(TypeError) {Regexp.linear_time?(nil)} assert_raise(TypeError) {Regexp.linear_time?(Regexp.allocate)} end @@ -1787,4 +2302,17 @@ class TestRegexp < Test::Unit::TestCase re =~ s end end + + def test_bug_16145_and_bug_21176_caseinsensitive_small # [Bug#16145] [Bug#21176] + encodings = [Encoding::UTF_8, Encoding::ISO_8859_1] + encodings.each do |enc| + o_acute_lower = "\u00F3".encode(enc) + o_acute_upper = "\u00D3".encode(enc) + assert_match(/[x#{o_acute_lower}]/i, "abc#{o_acute_upper}", "should match o acute case insensitive") + + e_acute_lower = "\u00E9".encode(enc) + e_acute_upper = "\u00C9".encode(enc) + assert_match(/[x#{e_acute_lower}]/i, "CAF#{e_acute_upper}", "should match e acute case insensitive") + end + end end diff --git a/test/ruby/test_require.rb b/test/ruby/test_require.rb index 60e5b0f8b1..0067a49700 100644 --- a/test/ruby/test_require.rb +++ b/test/ruby/test_require.rb @@ -6,11 +6,27 @@ require 'tmpdir' class TestRequire < Test::Unit::TestCase def test_load_error_path - filename = "should_not_exist" - error = assert_raise(LoadError) do - require filename - end - assert_equal filename, error.path + Tempfile.create(["should_not_exist", ".rb"]) {|t| + filename = t.path + t.close + File.unlink(filename) + + error = assert_raise(LoadError) do + require filename + end + assert_equal filename, error.path + + # with --disable=gems + assert_separately(["-", filename], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + filename = ARGV[0] + path = Struct.new(:to_path).new(filename) + error = assert_raise(LoadError) do + require path + end + assert_equal filename, error.path + end; + } end def test_require_invalid_shared_object @@ -52,7 +68,8 @@ class TestRequire < Test::Unit::TestCase def test_require_nonascii bug3758 = '[ruby-core:31915]' ["\u{221e}", "\x82\xa0".force_encoding("cp932")].each do |path| - assert_raise_with_message(LoadError, /#{path}\z/, bug3758) {require path} + e = assert_raise(LoadError, bug3758) {require path} + assert_operator(e.message, :end_with?, path, bug3758) end end @@ -192,7 +209,7 @@ class TestRequire < Test::Unit::TestCase File.write(req, "p :ok\n") assert_file.exist?(req) req[/.rb$/i] = "" - assert_in_out_err(['--disable-gems'], <<-INPUT, %w(:ok), []) + assert_in_out_err([], <<-INPUT, %w(:ok), []) require "#{req}" require "#{req}" INPUT @@ -353,6 +370,26 @@ class TestRequire < Test::Unit::TestCase end end + def test_public_in_wrapped_load + Tempfile.create(["test_public_in_wrapped_load", ".rb"]) do |t| + t.puts "def foo; end", "public :foo" + t.close + assert_warning(/main\.public/) do + assert load(t.path, true) + end + end + end + + def test_private_in_wrapped_load + Tempfile.create(["test_private_in_wrapped_load", ".rb"]) do |t| + t.puts "def foo; end", "private :foo" + t.close + assert_warning(/main\.private/) do + assert load(t.path, true) + end + end + end + def test_load_scope bug1982 = '[ruby-core:25039] [Bug #1982]' Tempfile.create(["test_ruby_test_require", ".rb"]) {|t| @@ -495,7 +532,7 @@ class TestRequire < Test::Unit::TestCase def test_frozen_loaded_features bug3756 = '[ruby-core:31913]' - assert_in_out_err(['-e', '$LOADED_FEATURES.freeze; require "ostruct"'], "", + assert_in_out_err(['-e', '$LOADED_FEATURES.freeze; require "erb"'], "", [], /\$LOADED_FEATURES is frozen; cannot append feature \(RuntimeError\)$/, bug3756) end @@ -680,7 +717,7 @@ class TestRequire < Test::Unit::TestCase Dir.mktmpdir {|tmp| Dir.chdir(tmp) { open("foo.rb", "w") {} - assert_in_out_err([{"RUBYOPT"=>nil}, '--disable-gems'], "#{<<~"begin;"}\n#{<<~"end;"}", %w(:ok), [], bug7158) + assert_in_out_err([{"RUBYOPT"=>nil}], "#{<<~"begin;"}\n#{<<~"end;"}", %w(:ok), [], bug7158) begin; $:.replace([IO::NULL]) a = Object.new @@ -708,7 +745,7 @@ class TestRequire < Test::Unit::TestCase Dir.mktmpdir {|tmp| Dir.chdir(tmp) { open("foo.rb", "w") {} - assert_in_out_err([{"RUBYOPT"=>nil}, '--disable-gems'], "#{<<~"begin;"}\n#{<<~"end;"}", %w(:ok), [], bug7158) + assert_in_out_err([{"RUBYOPT"=>nil}], "#{<<~"begin;"}\n#{<<~"end;"}", %w(:ok), [], bug7158) begin; $:.replace([IO::NULL]) a = Object.new @@ -738,7 +775,7 @@ class TestRequire < Test::Unit::TestCase open("foo.rb", "w") {} Dir.mkdir("a") open(File.join("a", "bar.rb"), "w") {} - assert_in_out_err(['--disable-gems'], "#{<<~"begin;"}\n#{<<~"end;"}", %w(:ok), [], bug7383) + assert_in_out_err([], "#{<<~"begin;"}\n#{<<~"end;"}", %w(:ok), [], bug7383) begin; $:.replace([IO::NULL]) $:.#{add} "#{tmp}" @@ -803,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 @@ -810,7 +877,7 @@ class TestRequire < Test::Unit::TestCase f.close File.unlink(f.path) File.mkfifo(f.path) - assert_separately(["-", f.path], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 3) + assert_separately(["-", f.path], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; th = Thread.current Thread.start {begin sleep(0.001) end until th.stop?; th.raise(IOError)} @@ -829,7 +896,7 @@ class TestRequire < Test::Unit::TestCase File.unlink(f.path) File.mkfifo(f.path) - assert_separately(["-", f.path], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 3) + assert_separately(["-", f.path], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; path = ARGV[0] th = Thread.current @@ -959,4 +1026,33 @@ class TestRequire < Test::Unit::TestCase assert_nil($LOAD_PATH.resolve_feature_path("superkalifragilisticoespialidoso")) end end + + def test_require_with_public_method_missing + # [Bug #19793] + assert_ruby_status(["-W0", "-rtempfile"], <<~RUBY, timeout: 60) + GC.stress = true + + class Object + public :method_missing + end + + Tempfile.create(["empty", ".rb"]) do |file| + require file.path + 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_require_lib.rb b/test/ruby/test_require_lib.rb index 95fa3f29e1..44dfbcf9ec 100644 --- a/test/ruby/test_require_lib.rb +++ b/test/ruby/test_require_lib.rb @@ -1,25 +1,26 @@ -# frozen_string_literal: false +# frozen_string_literal: true require 'test/unit' class TestRequireLib < Test::Unit::TestCase - TEST_RATIO = ENV["TEST_REQUIRE_THREAD_RATIO"]&.tap {|s|break s.to_f} || 0.05 # testing all files needs too long time... + libdir = __dir__ + '/../../lib' - Dir.glob(File.expand_path('../../lib/**/*.rb', __dir__)).each do |lib| - # skip some problems - next if %r!/lib/(?:bundler|rubygems)\b! =~ lib - next if %r!/lib/(?:debug|mkmf)\.rb\z! =~ lib - next if %r!/lib/irb/ext/tracer\.rb\z! =~ lib - # skip many files that almost use no threads - next if TEST_RATIO < rand(0.0..1.0) + # .rb files at lib + scripts = Dir.glob('*.rb', base: libdir).map {|f| f.chomp('.rb')} + + # .rb files in subdirectories of lib without same name script + dirs = Dir.glob('*/', base: libdir).map {|d| d.chomp('/')} + dirs -= scripts + scripts.concat(Dir.glob(dirs.map {|d| d + '/*.rb'}, base: libdir).map {|f| f.chomp('.rb')}) + + # skip some problems + scripts -= %w[bundler bundled_gems rubygems mkmf set/sorted_set] + + scripts.each do |lib| define_method "test_thread_size:#{lib}" do - assert_separately(['--disable-gems', '-W0'], "#{<<~"begin;"}\n#{<<~"end;"}") + assert_separately(['-W0'], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 60) begin; n = Thread.list.size - begin - require #{lib.dump} - rescue Exception - omit $! - end + require #{lib.dump} assert_equal n, Thread.list.size end; end diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index 64d8d93d3f..cd2dd5d3ff 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -5,16 +5,25 @@ require 'timeout' require 'tmpdir' require 'tempfile' require_relative '../lib/jit_support' +require_relative '../lib/parser_support' class TestRubyOptions < Test::Unit::TestCase - def self.rjit_enabled? = defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? - def self.yjit_enabled? = defined?(RubyVM::YJIT.enabled?) && 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. + RUBY_DESCRIPTION = + if ParserSupport.prism_enabled_in_subprocess? + ::RUBY_DESCRIPTION + else + ::RUBY_DESCRIPTION.sub(/\+PRISM /, '') + end NO_JIT_DESCRIPTION = - if rjit_enabled? - RUBY_DESCRIPTION.sub(/\+RJIT /, '') - elsif yjit_enabled? - RUBY_DESCRIPTION.sub(/\+YJIT( (dev|dev_nodebug|stats))? /, '') + case + when JITSupport.yjit_enabled? + RUBY_DESCRIPTION.sub(/\+YJIT( \w+)? /, '') + when JITSupport.zjit_enabled? + RUBY_DESCRIPTION.sub(/\+ZJIT( \w+)? /, '') else RUBY_DESCRIPTION end @@ -38,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 @@ -74,7 +88,7 @@ class TestRubyOptions < Test::Unit::TestCase def test_backtrace_limit assert_in_out_err(%w(--backtrace-limit), "", [], /missing argument for --backtrace-limit/) assert_in_out_err(%w(--backtrace-limit= 1), "", [], /missing argument for --backtrace-limit/) - assert_in_out_err(%w(--backtrace-limit=-1), "", [], /wrong limit for backtrace length/) + assert_in_out_err(%w(--backtrace-limit=-2), "", [], /wrong limit for backtrace length/) code = 'def f(n);n > 0 ? f(n-1) : raise;end;f(5)' assert_in_out_err(%w(--backtrace-limit=1), code, [], [/.*unhandled exception\n/, /^\tfrom .*\n/, @@ -84,46 +98,71 @@ class TestRubyOptions < Test::Unit::TestCase /^\t \.{3} \d+ levels\.{3}\n/]) assert_kind_of(Integer, Thread::Backtrace.limit) assert_in_out_err(%w(--backtrace-limit=1), "p Thread::Backtrace.limit", ['1'], []) + assert_in_out_err(%w(--backtrace-limit 1), "p Thread::Backtrace.limit", ['1'], []) + env = {"RUBYOPT" => "--backtrace-limit=5"} + assert_in_out_err([env], "p Thread::Backtrace.limit", ['5'], []) + assert_in_out_err([env, "--backtrace-limit=1"], "p Thread::Backtrace.limit", ['1'], []) + assert_in_out_err([env, "--backtrace-limit=-1"], "p Thread::Backtrace.limit", ['-1'], []) + assert_in_out_err([env, "--backtrace-limit=3", "--backtrace-limit=1"], + "p Thread::Backtrace.limit", ['1'], []) + assert_in_out_err([{"RUBYOPT" => "--backtrace-limit=5 --backtrace-limit=3"}], + "p Thread::Backtrace.limit", ['3'], []) + long_max = RbConfig::LIMITS["LONG_MAX"] + assert_in_out_err(%W(--backtrace-limit=#{long_max}), "p Thread::Backtrace.limit", + ["#{long_max}"], []) end def test_warning - save_rubyopt = ENV['RUBYOPT'] - ENV['RUBYOPT'] = nil + save_rubyopt = ENV.delete('RUBYOPT') assert_in_out_err(%w(-W0 -e) + ['p $-W'], "", %w(0), []) assert_in_out_err(%w(-W1 -e) + ['p $-W'], "", %w(1), []) assert_in_out_err(%w(-Wx -e) + ['p $-W'], "", %w(2), []) assert_in_out_err(%w(-W -e) + ['p $-W'], "", %w(2), []) assert_in_out_err(%w(-We) + ['p $-W'], "", %w(2), []) assert_in_out_err(%w(-w -W0 -e) + ['p $-W'], "", %w(0), []) - assert_in_out_err(%w(-W:deprecated -e) + ['p Warning[:deprecated]'], "", %w(true), []) - assert_in_out_err(%w(-W:no-deprecated -e) + ['p Warning[:deprecated]'], "", %w(false), []) - assert_in_out_err(%w(-W:experimental -e) + ['p Warning[:experimental]'], "", %w(true), []) - assert_in_out_err(%w(-W:no-experimental -e) + ['p Warning[:experimental]'], "", %w(false), []) - assert_in_out_err(%w(-W:qux), "", [], /unknown warning category: `qux'/) - assert_in_out_err(%w(-w -e) + ['p Warning[:deprecated]'], "", %w(true), []) - assert_in_out_err(%w(-W -e) + ['p Warning[:deprecated]'], "", %w(true), []) - assert_in_out_err(%w(-We) + ['p Warning[:deprecated]'], "", %w(true), []) - assert_in_out_err(%w(-e) + ['p Warning[:deprecated]'], "", %w(false), []) - code = 'puts "#{$VERBOSE}:#{Warning[:deprecated]}:#{Warning[:experimental]}"' + + categories = {deprecated: 1, experimental: 0, performance: 2, strict_unused_block: 3} + assert_equal categories.keys.sort, Warning.categories.sort + + categories.each do |category, level| + assert_in_out_err(["-W:#{category}", "-e", "p Warning[:#{category}]"], "", %w(true), []) + assert_in_out_err(["-W:no-#{category}", "-e", "p Warning[:#{category}]"], "", %w(false), []) + assert_in_out_err(["-e", "p Warning[:#{category}]"], "", level > 0 ? %w(false) : %w(true), []) + assert_in_out_err(["-w", "-e", "p Warning[:#{category}]"], "", level > 1 ? %w(false) : %w(true), []) + assert_in_out_err(["-W", "-e", "p Warning[:#{category}]"], "", level > 1 ? %w(false) : %w(true), []) + assert_in_out_err(["-We", "p Warning[:#{category}]"], "", level > 1 ? %w(false) : %w(true), []) + end + assert_in_out_err(%w(-W:qux), "", [], /unknown warning category: 'qux'/) + + def categories.expected(lev = 1, **warnings) + [ + (lev > 1).to_s, + *map {|category, level| warnings.fetch(category, lev > level).to_s} + ].join(':') + end + code = ['#{$VERBOSE}', *categories.map {|category, | "\#{Warning[:#{category}]}"}].join(':') + code = %[puts "#{code}"] Tempfile.create(["test_ruby_test_rubyoption", ".rb"]) do |t| t.puts code t.close - assert_in_out_err(["-r#{t.path}", '-e', code], "", %w(false:false:true false:false:true), []) - assert_in_out_err(["-r#{t.path}", '-w', '-e', code], "", %w(true:true:true true:true:true), []) - assert_in_out_err(["-r#{t.path}", '-W:deprecated', '-e', code], "", %w(false:true:true false:true:true), []) - assert_in_out_err(["-r#{t.path}", '-W:no-experimental', '-e', code], "", %w(false:false:false false:false:false), []) + assert_in_out_err(["-r#{t.path}", '-e', code], "", [categories.expected(1)]*2, []) + assert_in_out_err(["-r#{t.path}", '-w', '-e', code], "", [categories.expected(2)]*2, []) + categories.each do |category, | + assert_in_out_err(["-r#{t.path}", "-W:#{category}", '-e', code], "", [categories.expected(category => 'true')]*2, []) + assert_in_out_err(["-r#{t.path}", "-W:no-#{category}", '-e', code], "", [categories.expected(category => 'false')]*2, []) + end end ensure ENV['RUBYOPT'] = save_rubyopt end def test_debug - assert_in_out_err(["--disable-gems", "-de", "p $DEBUG"], "", %w(true), []) + assert_in_out_err(["-de", "p $DEBUG"], "", %w(true), []) - assert_in_out_err(["--disable-gems", "--debug", "-e", "p $DEBUG"], + assert_in_out_err(["--debug", "-e", "p $DEBUG"], "", %w(true), []) - assert_in_out_err(["--disable-gems", "--debug-", "-e", "p $DEBUG"], "", %w(), /invalid option --debug-/) + assert_in_out_err(["--debug-", "-e", "p $DEBUG"], "", %w(), /invalid option --debug-/) end q = Regexp.method(:quote) @@ -133,25 +172,14 @@ class TestRubyOptions < Test::Unit::TestCase /^jruby #{q[RUBY_ENGINE_VERSION]} \(#{q[RUBY_VERSION]}\).*? \[#{ q[RbConfig::CONFIG["host_os"]]}-#{q[RbConfig::CONFIG["host_cpu"]]}\]$/ else - /^ruby #{q[RUBY_VERSION]}(?:[p ]|dev|rc).*? \[#{q[RUBY_PLATFORM]}\]$/ + /^ruby #{q[RUBY_VERSION]}(?:[p ]|dev|rc).*? (\+PRISM )?\[#{q[RUBY_PLATFORM]}\]$/ end private_constant :VERSION_PATTERN - VERSION_PATTERN_WITH_RJIT = - case RUBY_ENGINE - when 'ruby' - /^ruby #{q[RUBY_VERSION]}(?:[p ]|dev|rc).*? \+RJIT \[#{q[RUBY_PLATFORM]}\]$/ - else - VERSION_PATTERN - end - private_constant :VERSION_PATTERN_WITH_RJIT - def test_verbose assert_in_out_err([{'RUBY_YJIT_ENABLE' => nil}, "-vve", ""]) do |r, e| assert_match(VERSION_PATTERN, r[0]) - if self.class.rjit_enabled? && !JITSupport.rjit_force_enabled? - assert_equal(NO_JIT_DESCRIPTION, r[0]) - elsif 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]) @@ -172,14 +200,16 @@ class TestRubyOptions < Test::Unit::TestCase end def test_enable - if JITSupport.yjit_supported? || JITSupport.rjit_supported? + if JITSupport.yjit_supported? assert_in_out_err(%w(--enable all -e) + [""], "", [], []) assert_in_out_err(%w(--enable-all -e) + [""], "", [], []) assert_in_out_err(%w(--enable=all -e) + [""], "", [], []) end assert_in_out_err(%w(--enable foobarbazqux -e) + [""], "", [], - /unknown argument for --enable: `foobarbazqux'/) + /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 @@ -187,16 +217,16 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err(%w(--disable-all -e) + [""], "", [], []) assert_in_out_err(%w(--disable=all -e) + [""], "", [], []) assert_in_out_err(%w(--disable foobarbazqux -e) + [""], "", [], - /unknown argument for --disable: `foobarbazqux'/) + /unknown argument for --disable: 'foobarbazqux'/) assert_in_out_err(%w(--disable), "", [], /missing argument for --disable/) - assert_in_out_err(%w(--disable-gems -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(--disable-gems -e) + ['p defined? DidYouMean'], "", ["nil"], []) + assert_in_out_err(%w(-e) + ['p defined? DidYouMean'], "", ["nil"], []) end def test_kanji assert_in_out_err(%w(-KU), "p '\u3042'") do |r, e| - assert_equal("\"\u3042\"", r.join.force_encoding(Encoding::UTF_8)) + assert_equal("\"\u3042\"", r.join('').force_encoding(Encoding::UTF_8)) end line = '-eputs"\xc2\xa1".encoding' env = {'RUBYOPT' => nil} @@ -217,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.rjit_enabled? || self.class.yjit_enabled? # checking -D(M|Y)JIT_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]) @@ -226,44 +256,28 @@ class TestRubyOptions < Test::Unit::TestCase end end - def test_rjit_disabled_version - return unless JITSupport.rjit_supported? - return if JITSupport.yjit_force_enabled? + def test_enabled_gc + omit unless /linux|darwin/ =~ RUBY_PLATFORM - env = { 'RUBY_YJIT_ENABLE' => nil } # unset in children - [ - %w(--version --rjit --disable=rjit), - %w(--version --enable=rjit --disable=rjit), - %w(--version --enable-rjit --disable-rjit), - ].each do |args| - assert_in_out_err([env] + args) do |r, e| - assert_match(VERSION_PATTERN, r[0]) - assert_match(NO_JIT_DESCRIPTION, r[0]) - assert_equal([], e) - end + if RbConfig::CONFIG['modular_gc_dir'].length > 0 + assert_match(/\+GC/, RUBY_DESCRIPTION) + else + assert_no_match(/\+GC/, RUBY_DESCRIPTION) end end - def test_rjit_version - return unless JITSupport.rjit_supported? - return if JITSupport.yjit_force_enabled? + def test_parser_flag + omit if ENV["RUBYOPT"]&.include?("--parser=") - env = { 'RUBY_YJIT_ENABLE' => nil } # unset in children - [ - %w(--version --rjit), - %w(--version --enable=rjit), - %w(--version --enable-rjit), - ].each do |args| - assert_in_out_err([env] + args) do |r, e| - assert_match(VERSION_PATTERN_WITH_RJIT, r[0]) - if JITSupport.rjit_force_enabled? - assert_equal(RUBY_DESCRIPTION, r[0]) - else - assert_equal(EnvUtil.invoke_ruby([env, '--rjit', '-e', 'print RUBY_DESCRIPTION'], '', true).first, r[0]) - end - assert_equal([], e) - end - end + assert_in_out_err(%w(--parser=prism -e) + ["puts :hi"], "", %w(hi), []) + assert_in_out_err(%w(--parser=prism --dump=parsetree -e _=:hi), "", /"hi"/, []) + + assert_in_out_err(%w(--parser=parse.y -e) + ["puts :hi"], "", %w(hi), []) + assert_norun_with_rflag('--parser=parse.y', '--version', "") + + assert_in_out_err(%w(--parser=notreal -e) + ["puts :hi"], "", [], /unknown parser notreal/) + + assert_in_out_err(%w(--parser=prism --version), "", /\+PRISM/, []) end def test_eval @@ -309,12 +323,25 @@ class TestRubyOptions < Test::Unit::TestCase d = Dir.tmpdir assert_in_out_err(["-C", d, "-e", "puts Dir.pwd"]) do |r, e| - assert_file.identical?(r.join, d) + assert_file.identical?(r.join(''), d) assert_equal([], e) end + + Dir.mktmpdir(nil, d) do |base| + # "test" in Japanese and N'Ko + test = base + "/\u{30c6 30b9 30c8}_\u{7e1 7ca 7dd 7cc 7df 7cd 7eb}" + Dir.mkdir(test) + assert_in_out_err(["-C", base, "-C", File.basename(test), "-e", "puts Dir.pwd"]) do |r, e| + assert_file.identical?(r.join(''), test) + assert_equal([], e) + end + Dir.rmdir(test) + end end def test_yydebug + omit if ParserSupport.prism_enabled_in_subprocess? + assert_in_out_err(["-ye", ""]) do |r, e| assert_not_equal([], r) assert_equal([], e) @@ -332,22 +359,32 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err(%w(--encoding test_ruby_test_rubyoptions_foobarbazqux), "", [], /unknown encoding name - test_ruby_test_rubyoptions_foobarbazqux \(RuntimeError\)/) - if /mswin|mingw|aix|android/ =~ RUBY_PLATFORM && - (str = "\u3042".force_encoding(Encoding.find("external"))).valid_encoding? - # This result depends on locale because LANG=C doesn't affect locale - # on Windows. - # On AIX, the source encoding of stdin with LANG=C is ISO-8859-1, - # which allows \u3042. - out, err = [str], [] - else - out, err = [], /invalid multibyte char/ - end - assert_in_out_err(%w(-Eutf-8), "puts '\u3042'", out, err) - assert_in_out_err(%w(--encoding utf-8), "puts '\u3042'", out, err) + assert_in_out_err(%w(-Eutf-8), 'puts Encoding::default_external', ["UTF-8"]) + assert_in_out_err(%w(-Ecesu-8), 'puts Encoding::default_external', ["CESU-8"]) + assert_in_out_err(%w(--encoding utf-8), 'puts Encoding::default_external', ["UTF-8"]) + assert_in_out_err(%w(--encoding cesu-8), 'puts Encoding::default_external', ["CESU-8"]) end def test_syntax_check - assert_in_out_err(%w(-c -e a=1+1 -e !a), "", ["Syntax OK"], []) + assert_in_out_err(%w(-cw -e a=1+1 -e !a), "", ["Syntax OK"], []) + assert_in_out_err(%w(-cw -e break), "", [], [:*, /(-e:1:|~) Invalid break/, :*]) + assert_in_out_err(%w(-cw -e next), "", [], [:*, /(-e:1:|~) Invalid next/, :*]) + assert_in_out_err(%w(-cw -e redo), "", [], [:*, /(-e:1:|~) Invalid redo/, :*]) + assert_in_out_err(%w(-cw -e retry), "", [], [:*, /(-e:1:|~) Invalid retry/, :*]) + assert_in_out_err(%w(-cw -e yield), "", [], [:*, /(-e:1:|~) Invalid yield/, :*]) + assert_in_out_err(%w(-cw -e begin -e break -e end), "", [], [:*, /(-e:2:|~) Invalid break/, :*]) + assert_in_out_err(%w(-cw -e begin -e next -e end), "", [], [:*, /(-e:2:|~) Invalid next/, :*]) + assert_in_out_err(%w(-cw -e begin -e redo -e end), "", [], [:*, /(-e:2:|~) Invalid redo/, :*]) + assert_in_out_err(%w(-cw -e begin -e retry -e end), "", [], [:*, /(-e:2:|~) Invalid retry/, :*]) + assert_in_out_err(%w(-cw -e begin -e yield -e end), "", [], [:*, /(-e:2:|~) Invalid yield/, :*]) + assert_in_out_err(%w(-cw -e !defined?(break)), "", ["Syntax OK"], []) + assert_in_out_err(%w(-cw -e !defined?(next)), "", ["Syntax OK"], []) + assert_in_out_err(%w(-cw -e !defined?(redo)), "", ["Syntax OK"], []) + assert_in_out_err(%w(-cw -e !defined?(retry)), "", ["Syntax OK"], []) + assert_in_out_err(%w(-cw -e !defined?(yield)), "", ["Syntax OK"], []) + assert_in_out_err(%w(-n -cw -e break), "", ["Syntax OK"], []) + assert_in_out_err(%w(-n -cw -e next), "", ["Syntax OK"], []) + assert_in_out_err(%w(-n -cw -e redo), "", ["Syntax OK"], []) end def test_invalid_option @@ -355,11 +392,15 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err(%W(-\r -e) + [""], "", [], []) - assert_in_out_err(%W(-\rx), "", [], /invalid option -[\r\n] \(-h will show valid options\) \(RuntimeError\)/) + assert_in_out_err(%W(-\rx), "", [], /invalid option -\\r \(-h will show valid options\) \(RuntimeError\)/) - assert_in_out_err(%W(-\x01), "", [], /invalid option -\x01 \(-h will show valid options\) \(RuntimeError\)/) + assert_in_out_err(%W(-\x01), "", [], /invalid option -\\x01 \(-h will show valid options\) \(RuntimeError\)/) assert_in_out_err(%w(-Z), "", [], /invalid option -Z \(-h will show valid options\) \(RuntimeError\)/) + + assert_in_out_err(%W(-\u{1f608}), "", [], + /invalid option -(\\xf0|\u{1f608}) \(-h will show valid options\) \(RuntimeError\)/, + encoding: Encoding::UTF_8) end def test_rubyopt @@ -394,13 +435,12 @@ class TestRubyOptions < Test::Unit::TestCase ENV['RUBYOPT'] = '-W:no-experimental' assert_in_out_err(%w(), "p Warning[:experimental]", ["false"]) ENV['RUBYOPT'] = '-W:qux' - assert_in_out_err(%w(), "", [], /unknown warning category: `qux'/) + assert_in_out_err(%w(), "", [], /unknown warning category: 'qux'/) + + ENV['RUBYOPT'] = 'w' + assert_in_out_err(%w(), "p $VERBOSE", ["true"]) ensure - if rubyopt_orig - ENV['RUBYOPT'] = rubyopt_orig - else - ENV.delete('RUBYOPT') - end + ENV['RUBYOPT'] = rubyopt_orig end def test_search @@ -488,6 +528,18 @@ 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 + assert_in_out_err(%w(-0 --enable), "", [], /missing argument for --enable/) + assert_in_out_err(%w(-0 --disable), "", [], /missing argument for --disable/) + assert_in_out_err(%w(-0 --dump), "", [], /missing argument for --dump/) + assert_in_out_err(%w(-0 --encoding), "", [], /missing argument for --encoding/) + assert_in_out_err(%w(-0 --external-encoding), "", [], /missing argument for --external-encoding/) + assert_in_out_err(%w(-0 --internal-encoding), "", [], /missing argument for --internal-encoding/) + assert_in_out_err(%w(-0 --backtrace-limit), "", [], /missing argument for --backtrace-limit/) end def test_assignment_in_conditional @@ -500,7 +552,7 @@ class TestRubyOptions < Test::Unit::TestCase t.puts " end" t.puts "end" t.flush - warning = ' warning: found `= literal\' in conditional, should be ==' + warning = ' warning: found \'= literal\' in conditional, should be ==' err = ["#{t.path}:1:#{warning}", "#{t.path}:4:#{warning}", ] @@ -517,16 +569,29 @@ class TestRubyOptions < Test::Unit::TestCase t.puts "if a = {}; end" t.puts "if a = {1=>2}; end" t.puts "if a = {3=>a}; end" + t.puts "if a = :sym; end" t.flush err = ["#{t.path}:1:#{warning}", "#{t.path}:2:#{warning}", "#{t.path}:3:#{warning}", "#{t.path}:5:#{warning}", "#{t.path}:6:#{warning}", + "#{t.path}:8:#{warning}", ] feature4299 = '[ruby-dev:43083]' assert_in_out_err(["-w", t.path], "", [], err, feature4299) assert_in_out_err(["-wr", t.path, "-e", ""], "", [], err, feature4299) + + t.rewind + t.truncate(0) + t.puts "if a = __LINE__; end" + t.puts "if a = __FILE__; end" + t.flush + err = ["#{t.path}:1:#{warning}", + "#{t.path}:2:#{warning}", + ] + assert_in_out_err(["-w", t.path], "", [], err) + assert_in_out_err(["-wr", t.path, "-e", ""], "", [], err) } end @@ -731,14 +796,22 @@ 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. + # see assert_pattern_list ExpectedStderrList = [ %r( - -e:(?:1:)?\s\[BUG\]\sSegmentation\sfault.*\n + (?:-e:(?:1:)?\s)?\[BUG\]\sSegmentation\sfault.*\n )x, %r( - #{ Regexp.quote((TestRubyOptions.rjit_enabled? && !JITSupport.rjit_force_enabled?) ? NO_JIT_DESCRIPTION : RUBY_DESCRIPTION) }\n\n + #{ Regexp.quote(RUBY_DESCRIPTION) }\n\n )x, %r( (?:--\s(?:.+\n)*\n)? @@ -749,8 +822,8 @@ class TestRubyOptions < Test::Unit::TestCase %r( (?: --\sRuby\slevel\sbacktrace\sinformation\s----------------------------------------\n - (?:-e:1:in\s\`(?:block\sin\s)?<main>\'\n)* - -e:1:in\s\`kill\'\n + (?:-e:1:in\s\'(?:block\sin\s)?<main>\'\n)* + -e:1:in\s\'kill\'\n \n )? )x, @@ -773,36 +846,47 @@ class TestRubyOptions < Test::Unit::TestCase )? )x, ] - end - def assert_segv(args, message=nil) - omit if ENV['RUBY_ON_BUG'] + KILL_SELF = "Process.kill :SEGV, $$" + end + def assert_segv(args, message=nil, list: SEGVTest::ExpectedStderrList, **opt, &block) # We want YJIT to be enabled in the subprocess if it's enabled for us # so that the Ruby description matches. - args.unshift("--yjit") if self.class.yjit_enabled? + env = Hash === args.first ? args.shift : {} + 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', 'LSAN_OPTIONS' => 'handle_segv=0'}) + args.unshift(env) test_stdin = "" - opt = SEGVTest::ExecOptions.dup - list = SEGVTest::ExpectedStderrList + if !block + tests = [//, list, message] + elsif message + tests = [[], [], message] + end - assert_in_out_err(args, test_stdin, //, list, encoding: "ASCII-8BIT", **opt) + assert_in_out_err(args, test_stdin, *tests, encoding: "ASCII-8BIT", + **SEGVTest::ExecOptions, **opt, &block) end def test_segv_test - assert_segv(["--disable-gems", "-e", "Process.kill :SEGV, $$"]) + assert_segv(["--disable-gems", "-e", SEGVTest::KILL_SELF]) end def test_segv_loaded_features bug7402 = '[ruby-core:49573]' - status = assert_segv(['-e', 'END {Process.kill :SEGV, $$}', - '-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 @@ -810,10 +894,85 @@ class TestRubyOptions < Test::Unit::TestCase Tempfile.create(["test_ruby_test_bug7597", ".rb"]) {|t| t.write "f" * 100 t.flush - assert_segv(["--disable-gems", "-e", "$0=ARGV[0]; Process.kill :SEGV, $$", t.path], bug7597) + assert_segv(["--disable-gems", "-e", "$0=ARGV[0]; #{SEGVTest::KILL_SELF}", t.path], bug7597) } end + def assert_crash_report(path, cmd = nil, &block) + Dir.mktmpdir("ruby_crash_report") do |dir| + list = SEGVTest::ExpectedStderrList + if cmd + FileUtils.mkpath(File.join(dir, File.dirname(cmd))) + File.write(File.join(dir, cmd), SEGVTest::KILL_SELF+"\n") + c = Regexp.quote(cmd) + list = list.map {|re| Regexp.new(re.source.gsub(/^\s*(\(\?:)?\K-e(?=:)/) {c}, re.options)} + else + cmd = ['-e', SEGVTest::KILL_SELF] + end + status = assert_segv([{"RUBY_CRASH_REPORT"=>path}, *cmd], list: [], chdir: dir, &block) + next if block + reports = Dir.glob("*.log", File::FNM_DOTMATCH, base: dir) + assert_equal(1, reports.size) + assert_pattern_list(list, File.read(File.join(dir, reports.first))) + break status, reports.first + end + end + + def test_crash_report + status, report = assert_crash_report("%e.%f.%p.log") + assert_equal("#{File.basename(EnvUtil.rubybin)}.-e.#{status.pid}.log", report) + end + + def test_crash_report_script + status, report = assert_crash_report("%e.%f.%p.log", "bug.rb") + assert_equal("#{File.basename(EnvUtil.rubybin)}.bug.rb.#{status.pid}.log", report) + end + + def test_crash_report_executable_path + omit if EnvUtil.rubybin.size > 245 + status, report = assert_crash_report("%E.%p.log") + path = EnvUtil.rubybin.sub(/\A\w\K:[\/\\]/, '!').tr_s('/', '!') + assert_equal("#{path}.#{status.pid}.log", report) + end + + def test_crash_report_script_path + status, report = assert_crash_report("%F.%p.log", "test/bug.rb") + assert_equal("test!bug.rb.#{status.pid}.log", report) + end + + def test_crash_report_pipe + if File.executable?(echo = "/bin/echo") + elsif /mswin|ming/ =~ RUBY_PLATFORM + echo = "echo" + else + omit "/bin/echo not found" + end + assert_crash_report("| #{echo} %e:%f:%p") do |stdin, stdout, status| + assert_equal(["#{File.basename(EnvUtil.rubybin)}:-e:#{status.pid}"], stdin) + 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" @@ -860,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 } @@ -996,17 +1155,17 @@ class TestRubyOptions < Test::Unit::TestCase assert_in_out_err(['-p', '-e', 'sub(/t.*/){"TEST"}'], %[test], %w[TEST], [], bug7157) end - def assert_norun_with_rflag(*opt) + def assert_norun_with_rflag(*opt, test_stderr: []) bug10435 = "[ruby-dev:48712] [Bug #10435]: should not run with #{opt} option" stderr = [] Tempfile.create(%w"bug10435- .rb") do |script| dir, base = File.split(script.path) File.write(script, "abort ':run'\n") opts = ['-C', dir, '-r', "./#{base}", *opt] - _, e = assert_in_out_err([*opts, '-ep'], "", //) + _, e = assert_in_out_err([*opts, '-ep'], "", //, test_stderr) stderr.concat(e) if e stderr << "---" - _, e = assert_in_out_err([*opts, base], "", //) + _, e = assert_in_out_err([*opts, base], "", //, test_stderr) stderr.concat(e) if e end assert_not_include(stderr, ":run", bug10435) @@ -1029,6 +1188,17 @@ class TestRubyOptions < Test::Unit::TestCase assert_norun_with_rflag('--dump=parse+error_tolerant') end + def test_dump_parsetree_error_tolerant + omit if ParserSupport.prism_enabled_in_subprocess? + + assert_in_out_err(['--dump=parse', '-e', 'begin'], + "", [], /unexpected end-of-input/, success: false) + assert_in_out_err(['--dump=parse', '--dump=+error_tolerant', '-e', 'begin'], + "", /^# @/, /unexpected end-of-input/, success: true) + assert_in_out_err(['--dump=+error_tolerant', '-e', 'begin p :run'], + "", [], /unexpected end-of-input/, success: false) + end + def test_dump_insns_with_rflag assert_norun_with_rflag('--dump=insns') end @@ -1062,8 +1232,8 @@ class TestRubyOptions < Test::Unit::TestCase frozen = [ ["--enable-frozen-string-literal", true], ["--disable-frozen-string-literal", false], - [nil, false], ] + debugs = [ ["--debug-frozen-string-literal", true], ["--debug=frozen-string-literal", true], @@ -1085,6 +1255,16 @@ class TestRubyOptions < Test::Unit::TestCase end end + def test_frozen_string_literal_debug_chilled_strings + code = <<~RUBY + "foo" << "bar" + RUBY + assert_in_out_err(["-W:deprecated"], code, [], ["-:1: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)"]) + assert_in_out_err(["-W:deprecated", "--debug-frozen-string-literal"], code, [], ["-:1: warning: literal string will be frozen in the future", "-:1: info: the string was created here"]) + assert_in_out_err(["-W:deprecated", "--disable-frozen-string-literal", "--debug-frozen-string-literal"], code, [], []) + assert_in_out_err(["-W:deprecated", "--enable-frozen-string-literal", "--debug-frozen-string-literal"], code, [], ["-:1:in '<main>': can't modify frozen String: \"foo\", created at -:1 (FrozenError)"]) + end + def test___dir__encoding lang = {"LC_ALL"=>ENV["LC_ALL"]||ENV["LANG"]} with_tmpchdir do @@ -1127,4 +1307,20 @@ class TestRubyOptions < Test::Unit::TestCase omit "#{IO::NULL} is not a character device" unless File.chardev?(IO::NULL) assert_in_out_err([IO::NULL], success: true) end + + def test_free_at_exit_env_var + env = {"RUBY_FREE_AT_EXIT"=>"1"} + assert_ruby_status([env, "-e;"]) + assert_in_out_err([env, "-W"], "", [], /Free at exit is experimental and may be unstable/) + end + + 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_rubyvm.rb b/test/ruby/test_rubyvm.rb index d729aa5af8..225cb45f33 100644 --- a/test/ruby/test_rubyvm.rb +++ b/test/ruby/test_rubyvm.rb @@ -1,5 +1,6 @@ # frozen_string_literal: false require 'test/unit' +require_relative '../lib/parser_support' class TestRubyVM < Test::Unit::TestCase def test_stat @@ -32,6 +33,7 @@ class TestRubyVM < Test::Unit::TestCase end def test_keep_script_lines + omit if ParserSupport.prism_enabled? pend if ENV['RUBY_ISEQ_DUMP_DEBUG'] # TODO prev_conf = RubyVM.keep_script_lines diff --git a/test/ruby/test_set.rb b/test/ruby/test_set.rb new file mode 100644 index 0000000000..70a61aa3b5 --- /dev/null +++ b/test/ruby/test_set.rb @@ -0,0 +1,1052 @@ +# frozen_string_literal: false +require 'test/unit' +require 'set' + +class TC_Set < Test::Unit::TestCase + class SetSubclass < Set + end + class CoreSetSubclass < Set::CoreSet + end + ALL_SET_CLASSES = [Set, SetSubclass, CoreSetSubclass].freeze + + def test_marshal + set = Set[1, 2, 3] + mset = Marshal.load(Marshal.dump(set)) + assert_equal(set, mset) + assert_equal(set.compare_by_identity?, mset.compare_by_identity?) + + set.compare_by_identity + mset = Marshal.load(Marshal.dump(set)) + assert_equal(set, mset) + assert_equal(set.compare_by_identity?, mset.compare_by_identity?) + + set.instance_variable_set(:@a, 1) + mset = Marshal.load(Marshal.dump(set)) + assert_equal(set, mset) + assert_equal(set.compare_by_identity?, mset.compare_by_identity?) + assert_equal(1, mset.instance_variable_get(:@a)) + + old_stdlib_set_data = "\x04\bo:\bSet\x06:\n@hash}\bi\x06Ti\aTi\bTF".b + set = Marshal.load(old_stdlib_set_data) + assert_equal(Set[1, 2, 3], set) + + old_stdlib_set_cbi_data = "\x04\bo:\bSet\x06:\n@hashC:\tHash}\ai\x06Ti\aTF".b + set = Marshal.load(old_stdlib_set_cbi_data) + assert_equal(Set[1, 2].compare_by_identity, set) + end + + def test_aref + assert_nothing_raised { + Set[] + Set[nil] + Set[1,2,3] + } + + assert_equal(0, Set[].size) + assert_equal(1, Set[nil].size) + assert_equal(1, Set[[]].size) + assert_equal(1, Set[[nil]].size) + + set = Set[2,4,6,4] + assert_equal(Set.new([2,4,6]), set) + end + + def test_s_new + assert_nothing_raised { + Set.new() + Set.new(nil) + Set.new([]) + Set.new([1,2]) + Set.new('a'..'c') + } + assert_raise(ArgumentError) { + Set.new(false) + } + assert_raise(ArgumentError) { + Set.new(1) + } + assert_raise(ArgumentError) { + Set.new(1,2) + } + + assert_equal(0, Set.new().size) + assert_equal(0, Set.new(nil).size) + assert_equal(0, Set.new([]).size) + assert_equal(1, Set.new([nil]).size) + + ary = [2,4,6,4] + set = Set.new(ary) + ary.clear + assert_equal(false, set.empty?) + assert_equal(3, set.size) + + ary = [1,2,3] + + s = Set.new(ary) { |o| o * 2 } + assert_equal([2,4,6], s.sort) + end + + def test_clone + set1 = Set.new + set2 = set1.clone + set1 << 'abc' + assert_equal(Set.new, set2) + end + + def test_dup + set1 = Set[1,2] + set2 = set1.dup + + assert_not_same(set1, set2) + + assert_equal(set1, set2) + + set1.add(3) + + assert_not_equal(set1, set2) + end + + def test_size + assert_equal(0, Set[].size) + assert_equal(2, Set[1,2].size) + assert_equal(2, Set[1,2,1].size) + end + + def test_empty? + assert_equal(true, Set[].empty?) + assert_equal(false, Set[1, 2].empty?) + end + + def test_clear + set = Set[1,2] + ret = set.clear + + assert_same(set, ret) + assert_equal(true, set.empty?) + end + + def test_replace + set = Set[1,2] + ret = set.replace('a'..'c') + + assert_same(set, ret) + 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) + } + assert_equal(Set[1,2], set) + end + + def test_to_a + set = Set[1,2,3,2] + ary = set.to_a + + assert_equal([1,2,3], ary.sort) + end + + def test_flatten + # test1 + set1 = Set[ + 1, + Set[ + 5, + Set[7, + Set[0] + ], + Set[6,2], + 1 + ], + 3, + Set[3,4] + ] + + set2 = set1.flatten + set3 = Set.new(0..7) + + assert_not_same(set2, set1) + assert_equal(set3, set2) + + # test2; destructive + orig_set1 = set1 + set1.flatten! + + assert_same(orig_set1, set1) + assert_equal(set3, set1) + + # test3; multiple occurrences of a set in an set + set1 = Set[1, 2] + set2 = Set[set1, Set[set1, 4], 3] + + assert_nothing_raised { + set2.flatten! + } + + assert_equal(Set.new(1..4), set2) + + # test4; recursion + set2 = Set[] + set1 = Set[1, set2] + set2.add(set1) + + assert_raise(ArgumentError) { + set1.flatten! + } + + # test5; miscellaneous + empty = Set[] + set = Set[Set[empty, "a"],Set[empty, "b"]] + + assert_nothing_raised { + set.flatten + } + + set1 = empty.merge(Set["no_more", set]) + + assert_nil(Set.new(0..31).flatten!) + + x = Set[Set[],Set[1,2]].flatten! + y = Set[1,2] + + assert_equal(x, y) + end + + def test_include? + set = Set[1,2,3] + + assert_equal(true, set.include?(1)) + assert_equal(true, set.include?(2)) + assert_equal(true, set.include?(3)) + assert_equal(false, set.include?(0)) + assert_equal(false, set.include?(nil)) + + set = Set["1",nil,"2",nil,"0","1",false] + assert_equal(true, set.include?(nil)) + assert_equal(true, set.include?(false)) + assert_equal(true, set.include?("1")) + assert_equal(false, set.include?(0)) + assert_equal(false, set.include?(true)) + end + + def test_eqq + set = Set[1,2,3] + + assert_equal(true, set === 1) + assert_equal(true, set === 2) + assert_equal(true, set === 3) + assert_equal(false, set === 0) + assert_equal(false, set === nil) + + set = Set["1",nil,"2",nil,"0","1",false] + assert_equal(true, set === nil) + assert_equal(true, set === false) + assert_equal(true, set === "1") + assert_equal(false, set === 0) + assert_equal(false, set === true) + end + + def test_superset? + set = Set[1,2,3] + + assert_raise(ArgumentError) { + set.superset?() + } + + assert_raise(ArgumentError) { + set.superset?(2) + } + + assert_raise(ArgumentError) { + set.superset?([2]) + } + + 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) + assert_equal(false, set.superset?(klass[1,2,3,4]), klass.name) + assert_equal(false, set.superset?(klass[1,4]), klass.name) + + assert_equal(true, set >= klass[1,2,3], klass.name) + assert_equal(true, set >= klass[1,2], klass.name) + + assert_equal(true, Set[].superset?(klass[]), klass.name) + } + end + + def test_proper_superset? + set = Set[1,2,3] + + assert_raise(ArgumentError) { + set.proper_superset?() + } + + assert_raise(ArgumentError) { + set.proper_superset?(2) + } + + assert_raise(ArgumentError) { + set.proper_superset?([2]) + } + + 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) + assert_equal(false, set.proper_superset?(klass[1,2,3,4]), klass.name) + assert_equal(false, set.proper_superset?(klass[1,4]), klass.name) + + assert_equal(false, set > klass[1,2,3], klass.name) + assert_equal(true, set > klass[1,2], klass.name) + + assert_equal(false, Set[].proper_superset?(klass[]), klass.name) + } + end + + def test_subset? + set = Set[1,2,3] + + assert_raise(ArgumentError) { + set.subset?() + } + + assert_raise(ArgumentError) { + set.subset?(2) + } + + assert_raise(ArgumentError) { + set.subset?([2]) + } + + 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) + assert_equal(false, set.subset?(klass[]), klass.name) + + assert_equal(true, set <= klass[1,2,3], klass.name) + assert_equal(true, set <= klass[1,2,3,4], klass.name) + + assert_equal(true, Set[].subset?(klass[1]), klass.name) + assert_equal(true, Set[].subset?(klass[]), klass.name) + } + end + + def test_proper_subset? + set = Set[1,2,3] + + assert_raise(ArgumentError) { + set.proper_subset?() + } + + assert_raise(ArgumentError) { + set.proper_subset?(2) + } + + assert_raise(ArgumentError) { + set.proper_subset?([2]) + } + + 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) + assert_equal(false, set.proper_subset?(klass[]), klass.name) + + assert_equal(false, set < klass[1,2,3], klass.name) + assert_equal(true, set < klass[1,2,3,4], klass.name) + + assert_equal(false, Set[].proper_subset?(klass[]), klass.name) + } + end + + def test_spacecraft_operator + set = Set[1,2,3] + + assert_nil(set <=> 2) + + assert_nil(set <=> set.to_a) + + 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) + assert_equal(+1, set <=> klass[2,3] , klass.name) + assert_equal(+1, set <=> klass[] , klass.name) + + assert_equal(0, Set[] <=> klass[], klass.name) + } + end + + def assert_intersect(expected, set, other) + case expected + when true + assert_send([set, :intersect?, other]) + assert_send([set, :intersect?, other.to_a]) + assert_send([other, :intersect?, set]) + assert_not_send([set, :disjoint?, other]) + assert_not_send([set, :disjoint?, other.to_a]) + assert_not_send([other, :disjoint?, set]) + when false + assert_not_send([set, :intersect?, other]) + assert_not_send([set, :intersect?, other.to_a]) + assert_not_send([other, :intersect?, set]) + assert_send([set, :disjoint?, other]) + assert_send([set, :disjoint?, other.to_a]) + assert_send([other, :disjoint?, set]) + when Class + assert_raise(expected) { + set.intersect?(other) + } + assert_raise(expected) { + set.disjoint?(other) + } + else + raise ArgumentError, "%s: unsupported expected value: %s" % [__method__, expected.inspect] + end + end + + def test_intersect? + set = Set[3,4,5] + + assert_intersect(ArgumentError, set, 3) + assert_intersect(true, set, Set[2,4,6]) + + assert_intersect(true, set, set) + assert_intersect(true, set, Set[2,4]) + assert_intersect(true, set, Set[5,6,7]) + assert_intersect(true, set, Set[1,2,6,8,4]) + + assert_intersect(false, set, Set[]) + assert_intersect(false, set, Set[0,2]) + assert_intersect(false, set, Set[0,2,6]) + assert_intersect(false, set, Set[0,2,6,8,10]) + + # Make sure set hasn't changed + assert_equal(Set[3,4,5], set) + end + + def test_each + ary = [1,3,5,7,10,20] + set = Set.new(ary) + + ret = set.each { |o| } + assert_same(set, ret) + + e = set.each + assert_instance_of(Enumerator, e) + + assert_nothing_raised { + set.each { |o| + ary.delete(o) or raise "unexpected element: #{o}" + } + + ary.empty? or raise "forgotten elements: #{ary.join(', ')}" + } + + assert_equal(6, e.size) + set << 42 + assert_equal(7, e.size) + end + + def test_add + set = Set[1,2,3] + + ret = set.add(2) + assert_same(set, ret) + assert_equal(Set[1,2,3], set) + + ret = set.add?(2) + assert_nil(ret) + assert_equal(Set[1,2,3], set) + + ret = set.add(4) + assert_same(set, ret) + assert_equal(Set[1,2,3,4], set) + + ret = set.add?(5) + assert_same(set, ret) + assert_equal(Set[1,2,3,4,5], set) + end + + def test_delete + set = Set[1,2,3] + + ret = set.delete(4) + assert_same(set, ret) + assert_equal(Set[1,2,3], set) + + ret = set.delete?(4) + assert_nil(ret) + assert_equal(Set[1,2,3], set) + + ret = set.delete(2) + assert_equal(set, ret) + assert_equal(Set[1,3], set) + + ret = set.delete?(1) + assert_equal(set, ret) + assert_equal(Set[3], set) + end + + def test_delete_if + set = Set.new(1..10) + ret = set.delete_if { |i| i > 10 } + assert_same(set, ret) + assert_equal(Set.new(1..10), set) + + set = Set.new(1..10) + ret = set.delete_if { |i| i % 3 == 0 } + assert_same(set, ret) + assert_equal(Set[1,2,4,5,7,8,10], set) + + set = Set.new(1..10) + enum = set.delete_if + assert_equal(set.size, enum.size) + assert_same(set, enum.each { |i| i % 3 == 0 }) + assert_equal(Set[1,2,4,5,7,8,10], set) + end + + def test_keep_if + set = Set.new(1..10) + ret = set.keep_if { |i| i <= 10 } + assert_same(set, ret) + assert_equal(Set.new(1..10), set) + + set = Set.new(1..10) + ret = set.keep_if { |i| i % 3 != 0 } + assert_same(set, ret) + assert_equal(Set[1,2,4,5,7,8,10], set) + + set = Set.new(1..10) + enum = set.keep_if + assert_equal(set.size, enum.size) + assert_same(set, enum.each { |i| i % 3 != 0 }) + assert_equal(Set[1,2,4,5,7,8,10], set) + end + + def test_collect! + set = Set[1,2,3,'a','b','c',-1..1,2..4] + + ret = set.collect! { |i| + case i + when Numeric + i * 2 + when String + i.upcase + else + nil + end + } + + assert_same(set, ret) + assert_equal(Set[2,4,6,'A','B','C',nil], set) + + set = Set[1,2,3,'a','b','c',-1..1,2..4] + enum = set.collect! + + assert_equal(set.size, enum.size) + assert_same(set, enum.each { |i| + case i + when Numeric + i * 2 + when String + i.upcase + else + nil + end + }) + assert_equal(Set[2,4,6,'A','B','C',nil], set) + end + + def test_reject! + set = Set.new(1..10) + + ret = set.reject! { |i| i > 10 } + assert_nil(ret) + assert_equal(Set.new(1..10), set) + + ret = set.reject! { |i| i % 3 == 0 } + assert_same(set, ret) + assert_equal(Set[1,2,4,5,7,8,10], set) + + set = Set.new(1..10) + enum = set.reject! + assert_equal(set.size, enum.size) + assert_same(set, enum.each { |i| i % 3 == 0 }) + assert_equal(Set[1,2,4,5,7,8,10], set) + end + + def test_select! + set = Set.new(1..10) + ret = set.select! { |i| i <= 10 } + assert_equal(nil, ret) + assert_equal(Set.new(1..10), set) + + set = Set.new(1..10) + ret = set.select! { |i| i % 3 != 0 } + assert_same(set, ret) + assert_equal(Set[1,2,4,5,7,8,10], set) + + set = Set.new(1..10) + enum = set.select! + assert_equal(set.size, enum.size) + assert_equal(nil, enum.each { |i| i <= 10 }) + assert_equal(Set.new(1..10), set) + end + + def test_filter! + set = Set.new(1..10) + ret = set.filter! { |i| i <= 10 } + assert_equal(nil, ret) + assert_equal(Set.new(1..10), set) + + set = Set.new(1..10) + ret = set.filter! { |i| i % 3 != 0 } + assert_same(set, ret) + assert_equal(Set[1,2,4,5,7,8,10], set) + + set = Set.new(1..10) + enum = set.filter! + assert_equal(set.size, enum.size) + assert_equal(nil, enum.each { |i| i <= 10 }) + assert_equal(Set.new(1..10), set) + end + + def test_merge + set = Set[1,2,3] + ret = set.merge([2,4,6]) + assert_same(set, ret) + assert_equal(Set[1,2,3,4,6], set) + + set = Set[1,2,3] + ret = set.merge() + assert_same(set, ret) + assert_equal(Set[1,2,3], set) + + set = Set[1,2,3] + ret = set.merge([2,4,6], Set[4,5,6]) + assert_same(set, ret) + assert_equal(Set[1,2,3,4,5,6], set) + + assert_raise(ArgumentError) { + Set[].merge(a: 1) + } + end + + def test_merge_mutating_hash_bug_21305 + a = (1..100).to_a + o = Object.new + o.define_singleton_method(:hash) do + a.clear + 0 + end + a.unshift o + assert_equal([o], Set.new.merge(a).to_a) + end + + def test_initialize_mutating_array_bug_21306 + a = (1..100).to_a + assert_equal(Set[0], Set.new(a){a.clear; 0}) + end + + def test_subtract + set = Set[1,2,3] + + ret = set.subtract([2,4,6]) + assert_same(set, ret) + assert_equal(Set[1,3], set) + end + + def test_plus + set = Set[1,2,3] + + ret = set + [2,4,6] + assert_not_same(set, ret) + assert_equal(Set[1,2,3,4,6], ret) + end + + def test_minus + set = Set[1,2,3] + + ret = set - [2,4,6] + assert_not_same(set, ret) + assert_equal(Set[1,3], ret) + end + + def test_and + set = Set[1,2,3,4] + + ret = set & [2,4,6] + assert_not_same(set, ret) + assert_equal(Set[2,4], ret) + end + + def test_xor + 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 + + assert_equal(original_b, b) + assert_equal(Set[2], result) + end + + def test_eq + set1 = Set[2,3,1] + set2 = Set[1,2,3] + + assert_equal(set1, set1) + assert_equal(set1, set2) + assert_not_equal(Set[1], [1]) + + set1 = Class.new(Set)["a", "b"] + set1.add(set1).reset # Make recursive + set2 = Set["a", "b", Set["a", "b", set1]] + + assert_equal(set1, set2) + + assert_not_equal(Set[Exception.new,nil], Set[Exception.new,Exception.new], "[ruby-dev:26127]") + end + + def test_classify + set = Set.new(1..10) + ret = set.classify { |i| i % 3 } + + assert_equal(3, ret.size) + assert_instance_of(Hash, ret) + ret.each_value { |value| assert_instance_of(Set, value) } + assert_equal(Set[3,6,9], ret[0]) + assert_equal(Set[1,4,7,10], ret[1]) + assert_equal(Set[2,5,8], ret[2]) + + set = Set.new(1..10) + enum = set.classify + + assert_equal(set.size, enum.size) + ret = enum.each { |i| i % 3 } + assert_equal(3, ret.size) + assert_instance_of(Hash, ret) + ret.each_value { |value| assert_instance_of(Set, value) } + assert_equal(Set[3,6,9], ret[0]) + assert_equal(Set[1,4,7,10], ret[1]) + assert_equal(Set[2,5,8], ret[2]) + end + + def test_divide + set = Set.new(1..10) + ret = set.divide { |i| i % 3 } + + assert_equal(3, ret.size) + n = 0 + ret.each { |s| n += s.size } + assert_equal(set.size, n) + assert_equal(set, ret.flatten) + + set = Set[7,10,5,11,1,3,4,9,0] + ret = set.divide { |a,b| (a - b).abs == 1 } + + assert_equal(4, ret.size) + n = 0 + ret.each { |s| n += s.size } + assert_equal(set.size, n) + assert_equal(set, ret.flatten) + ret.each { |s| + if s.include?(0) + assert_equal(Set[0,1], s) + elsif s.include?(3) + assert_equal(Set[3,4,5], s) + elsif s.include?(7) + assert_equal(Set[7], s) + elsif s.include?(9) + assert_equal(Set[9,10,11], s) + else + raise "unexpected group: #{s.inspect}" + end + } + + set = Set.new(1..10) + enum = set.divide + ret = enum.each { |i| i % 3 } + + assert_equal(set.size, enum.size) + assert_equal(3, ret.size) + n = 0 + 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 + orig = set = Set[1,2,3] + assert_equal false, set.frozen? + set << 4 + assert_same orig, set.freeze + assert_equal true, set.frozen? + assert_raise(FrozenError) { + set << 5 + } + assert_equal 4, set.size + end + + def test_freeze_dup + set1 = Set[1,2,3] + set1.freeze + set2 = set1.dup + + assert_not_predicate set2, :frozen? + assert_nothing_raised { + set2.add 4 + } + end + + def test_freeze_clone + set1 = Set[1,2,3] + set1.freeze + set2 = set1.clone + + assert_predicate set2, :frozen? + assert_raise(FrozenError) { + set2.add 5 + } + end + + def test_freeze_clone_false + set1 = Set[1,2,3] + set1.freeze + set2 = set1.clone(freeze: false) + + assert_not_predicate set2, :frozen? + set2.add 5 + assert_equal Set[1,2,3,5], set2 + assert_equal Set[1,2,3], set1 + end if Kernel.instance_method(:initialize_clone).arity != 1 + + def test_join + assert_equal('123', Set[1, 2, 3].join) + assert_equal('1 & 2 & 3', Set[1, 2, 3].join(' & ')) + end + + def test_inspect + set1 = Set[1, 2] + 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) + + set1.add(set2) + 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) + + set2 = Set[Set[0], 1, 2, set1] + 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) + end + + def test_compare_by_identity + a1, a2 = "a", "a" + b1, b2 = "b", "b" + c = "c" + array = [a1, b1, c, a2, b2] + + iset = Set.new.compare_by_identity + assert_send([iset, :compare_by_identity?]) + iset.merge(array) + assert_equal(5, iset.size) + assert_equal(array.map(&:object_id).sort, iset.map(&:object_id).sort) + + set = Set.new + assert_not_send([set, :compare_by_identity?]) + set.merge(array) + assert_equal(3, set.size) + assert_equal(array.uniq.sort, set.sort) + end + + def test_reset + [Set, Class.new(Set)].each { |klass| + a = [1, 2] + b = [1] + set = klass.new([a, b]) + + b << 2 + set.reset + + assert_equal(klass.new([a]), set, klass.name) + } + end + + def test_set_gc_compact_does_not_allocate + assert_in_out_err([], <<-"end;", [], []) + def x + s = Set.new + s << Object.new + s + end + + x + begin + GC.compact + rescue NotImplementedError + 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 + def test_to_set + ary = [2,5,4,3,2,1,3] + + set = ary.to_set + assert_instance_of(Set, set) + assert_equal([1,2,3,4,5], set.sort) + + set = ary.to_set { |o| o * -2 } + assert_instance_of(Set, set) + assert_equal([-10,-8,-6,-4,-2], set.sort) + + 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 + private def should_omit? + (RUBY_VERSION.scan(/\d+/).map(&:to_i) <=> [3, 2]) < 0 || + !File.exist?(File.expand_path('../prelude.rb', __dir__)) + end + + def test_Set + omit "skipping the test for the builtin Set" if should_omit? + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_nothing_raised do + set = Set.new([1, 2]) + assert_equal('Set', set.class.name) + end + end; + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_nothing_raised do + set = Set[1, 2] + assert_equal('Set', set.class.name) + end + end; + end + + def test_to_set + omit "skipping the test for the builtin Enumerable#to_set" if should_omit? + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_nothing_raised do + set = [1, 2].to_set + assert_equal('Set', set.class.name) + end + end; + end +end diff --git a/test/ruby/test_settracefunc.rb b/test/ruby/test_settracefunc.rb index a30c4309ad..d3b2441e21 100644 --- a/test/ruby/test_settracefunc.rb +++ b/test/ruby/test_settracefunc.rb @@ -1,5 +1,6 @@ # frozen_string_literal: false require 'test/unit' +EnvUtil.suppress_warning {require 'continuation'} class TestSetTraceFunc < Test::Unit::TestCase def setup @@ -93,6 +94,22 @@ class TestSetTraceFunc < Test::Unit::TestCase assert_equal([[:req]], parameters) end + def test_c_call_aliased_method + # [Bug #20915] + klass = Class.new do + alias_method :new_method, :method + end + + instance = klass.new + parameters = nil + + TracePoint.new(:c_call) do |tp| + parameters = tp.parameters + end.enable { instance.new_method(:to_s) } + + assert_equal([[:req]], parameters) + end + def test_call events = [] name = "#{self.class}\##{__method__}" @@ -231,7 +248,9 @@ class TestSetTraceFunc < Test::Unit::TestCase events.shift) assert_equal(["line", 5, :meth_return, self.class], events.shift) - assert_equal(["return", 7, :meth_return, self.class], + assert_equal(["line", 6, :meth_return, self.class], + events.shift) + assert_equal(["return", 6, :meth_return, self.class], events.shift) assert_equal(["line", 10, :test_return, self.class], events.shift) @@ -270,7 +289,7 @@ class TestSetTraceFunc < Test::Unit::TestCase events.shift) assert_equal(["line", 6, :meth_return2, self.class], events.shift) - assert_equal(["return", 7, :meth_return2, self.class], + assert_equal(["return", 6, :meth_return2, self.class], events.shift) assert_equal(["line", 9, :test_return2, self.class], events.shift) @@ -358,18 +377,18 @@ class TestSetTraceFunc < Test::Unit::TestCase def test_thread_trace events = {:set => [], :add => []} + name = "#{self.class}\##{__method__}" prc = Proc.new { |event, file, lineno, mid, binding, klass| - events[:set] << [event, lineno, mid, klass, :set] + events[:set] << [event, lineno, mid, klass, :set] if file == name } prc = prc # suppress warning prc2 = Proc.new { |event, file, lineno, mid, binding, klass| - events[:add] << [event, lineno, mid, klass, :add] + events[:add] << [event, lineno, mid, klass, :add] if file == name } prc2 = prc2 # suppress warning th = Thread.new do th = Thread.current - name = "#{self.class}\##{__method__}" eval <<-EOF.gsub(/^.*?: /, ""), nil, name 1: th.set_trace_func(prc) 2: th.add_trace_func(prc2) @@ -453,6 +472,9 @@ class TestSetTraceFunc < Test::Unit::TestCase bug3921 = '[ruby-dev:42350]' ok = false func = lambda{|e, f, l, i, b, k| + # In parallel testing, unexpected events like IO operations may be traced, + # so we filter out events here. + next unless f == __FILE__ set_trace_func(nil) ok = eval("self", b) } @@ -504,7 +526,7 @@ class TestSetTraceFunc < Test::Unit::TestCase 1: trace = TracePoint.trace(*trace_events){|tp| next if !target_thread? 2: events << [tp.event, tp.lineno, tp.path, _defined_class.(tp), tp.method_id, tp.self, tp.binding&.eval("_local_var"), _get_data.(tp)] if tp.path == 'xyzzy' 3: } - 4: 1.times{|;_local_var| _local_var = :inner + 4: [1].reverse_each{|;_local_var| _local_var = :inner 5: tap{} 6: } 7: class XYZZY @@ -531,10 +553,10 @@ class TestSetTraceFunc < Test::Unit::TestCase answer_events = [ # [:line, 4, 'xyzzy', self.class, method, self, :outer, :nothing], - [:c_call, 4, 'xyzzy', Integer, :times, 1, nil, :nothing], + [:c_call, 4, 'xyzzy', Array, :reverse_each, [1], nil, :nothing], [:line, 4, 'xyzzy', self.class, method, self, nil, :nothing], [:line, 5, 'xyzzy', self.class, method, self, :inner, :nothing], - [:c_return, 4, "xyzzy", Integer, :times, 1, nil, 1], + [:c_return, 4, "xyzzy", Array, :reverse_each, [1], nil, [1]], [:line, 7, 'xyzzy', self.class, method, self, :outer, :nothing], [:c_call, 7, "xyzzy", Module, :const_added, TestSetTraceFunc, nil, :nothing], [:c_return, 7, "xyzzy", Module, :const_added, TestSetTraceFunc, nil, nil], @@ -625,6 +647,19 @@ PREP CODE end + def test_tracepoint_bmethod_memory_leak + assert_no_memory_leak([], '', "#{<<~"begin;"}\n#{<<~'end;'}", "[Bug #20194]", rss: true) + obj = Object.new + obj.define_singleton_method(:foo) {} + bmethod = obj.method(:foo) + tp = TracePoint.new(:return) {} + begin; + 1_000_000.times do + tp.enable(target: bmethod) {} + end + end; + end + def trace_by_set_trace_func events = [] trace = nil @@ -639,7 +674,7 @@ CODE 1: set_trace_func(lambda{|event, file, line, id, binding, klass| 2: events << [event, line, file, klass, id, binding&.eval('self'), binding&.eval("_local_var")] if file == 'xyzzy' 3: }) - 4: 1.times{|;_local_var| _local_var = :inner + 4: [1].map!{|;_local_var| _local_var = :inner 5: tap{} 6: } 7: class XYZZY @@ -810,6 +845,9 @@ CODE args = nil trace = TracePoint.trace(:call){|tp| next if !target_thread? + # In parallel testing, unexpected events like IO operations may be traced, + # so we filter out events here. + next unless [TracePoint, TestSetTraceFunc].include?(tp.defined_class) ary << tp.method_id } foo @@ -955,6 +993,55 @@ CODE assert_equal(expected*2, events) end + def test_tracepoint_struct + c = Struct.new(:x) do + alias y x + alias y= x= + end + obj = c.new + + ar_meth = obj.method(:x) + aw_meth = obj.method(:x=) + aar_meth = obj.method(:y) + aaw_meth = obj.method(:y=) + events = [] + trace = TracePoint.new(:c_call, :c_return){|tp| + next if !target_thread? + next if tp.path != __FILE__ + next if tp.method_id == :call + case tp.event + when :c_call + assert_raise(RuntimeError) {tp.return_value} + events << [tp.event, tp.method_id, tp.callee_id] + when :c_return + events << [tp.event, tp.method_id, tp.callee_id, tp.return_value] + end + } + test_proc = proc do + obj.x = 1 + obj.x + obj.y = 2 + obj.y + aw_meth.call(1) + ar_meth.call + aaw_meth.call(2) + aar_meth.call + end + test_proc.call # populate call caches + trace.enable(&test_proc) + expected = [ + [:c_call, :x=, :x=], + [:c_return, :x=, :x=, 1], + [:c_call, :x, :x], + [:c_return, :x, :x, 1], + [:c_call, :x=, :y=], + [:c_return, :x=, :y=, 2], + [:c_call, :x, :y], + [:c_return, :x, :y, 2], + ] + assert_equal(expected*2, events) + end + class XYZZYException < Exception; end def method_test_tracepoint_raised_exception err raise err @@ -994,7 +1081,7 @@ CODE /return/ =~ tp.event ? tp.return_value : nil ] }.enable{ - 1.times{ + [1].map!{ 3 } method_for_test_tracepoint_block{ @@ -1004,10 +1091,10 @@ CODE # pp events # expected_events = [[:b_call, :test_tracepoint_block, TestSetTraceFunc, TestSetTraceFunc, nil], - [:c_call, :times, Integer, Integer, nil], + [:c_call, :map!, Array, Array, nil], [:b_call, :test_tracepoint_block, TestSetTraceFunc, TestSetTraceFunc, nil], [:b_return, :test_tracepoint_block, TestSetTraceFunc, TestSetTraceFunc, 3], - [:c_return, :times, Integer, Integer, 1], + [:c_return, :map!, Array, Array, [3]], [:call, :method_for_test_tracepoint_block, TestSetTraceFunc, TestSetTraceFunc, nil], [:b_call, :test_tracepoint_block, TestSetTraceFunc, TestSetTraceFunc, nil], [:b_return, :test_tracepoint_block, TestSetTraceFunc, TestSetTraceFunc, 4], @@ -1061,9 +1148,9 @@ CODE when :line assert_match(/ in /, str) when :call, :c_call - assert_match(/call \`/, str) # #<TracePoint:c_call `inherited' ../trunk/test.rb:11> + assert_match(/call \'/, str) # #<TracePoint:c_call 'inherited' ../trunk/test.rb:11> when :return, :c_return - assert_match(/return \`/, str) # #<TracePoint:return `m' ../trunk/test.rb:3> + assert_match(/return \'/, str) # #<TracePoint:return 'm' ../trunk/test.rb:3> when /thread/ assert_match(/\#<Thread:/, str) # #<TracePoint:thread_end of #<Thread:0x87076c0>> else @@ -1196,15 +1283,17 @@ CODE end } assert_normal_exit src % %q{obj.zip({}) {}}, bug7774 - assert_normal_exit src % %q{ - require 'continuation' - begin - c = nil - obj.sort_by {|x| callcc {|c2| c ||= c2 }; x } - c.call - rescue RuntimeError - end - }, bug7774 + if respond_to?(:callcc) + assert_normal_exit src % %q{ + require 'continuation' + begin + c = nil + obj.sort_by {|x| callcc {|c2| c ||= c2 }; x } + c.call + rescue RuntimeError + end + }, bug7774 + end # TracePoint tp_b = nil @@ -1306,11 +1395,13 @@ CODE def test_a_call events = [] + log = [] TracePoint.new(:a_call){|tp| next if !target_thread? events << tp.event + log << "| event:#{ tp.event } method_id:#{ tp.method_id } #{ tp.path }:#{ tp.lineno }" }.enable{ - 1.times{ + [1].map!{ 3 } method_for_test_tracepoint_block{ @@ -1323,16 +1414,18 @@ CODE :b_call, :call, :b_call, - ], events) + ], events, "TracePoint log:\n#{ log.join("\n") }\n") end def test_a_return events = [] + log = [] TracePoint.new(:a_return){|tp| next if !target_thread? events << tp.event + log << "| event:#{ tp.event } method_id:#{ tp.method_id } #{ tp.path }:#{ tp.lineno }" }.enable{ - 1.times{ + [1].map!{ 3 } method_for_test_tracepoint_block{ @@ -1345,7 +1438,7 @@ CODE :b_return, :return, :b_return - ], events) + ], events, "TracePoint log:\n#{ log.join("\n") }\n") end def test_const_missing @@ -1906,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{ @@ -1923,7 +2016,11 @@ CODE def tp_return_value mid ary = [] - TracePoint.new(:return, :b_return){|tp| next if !target_thread?; ary << [tp.event, tp.method_id, tp.return_value]}.enable{ + TracePoint.new(:return, :b_return){|tp| + next if !target_thread? + next if tp.path != __FILE__ + ary << [tp.event, tp.method_id, tp.return_value] + }.enable{ send mid } ary.pop # last b_return event is not required. @@ -2129,10 +2226,10 @@ 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] + events << [ev, line] if file == __FILE__ } # do not stop trace. They will be stopped at Thread termination. q.push 1 _x = 1 @@ -2168,9 +2265,6 @@ CODE } # it is dirty hack. usually we shouldn't use such technique Thread.pass until t.status == 'sleep' - # When RJIT thread exists, t.status becomes 'sleep' even if it does not reach m2t_q.pop. - # This sleep forces it to reach m2t_q.pop for --jit-wait. - sleep 1 if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? t.add_trace_func proc{|ev, file, line, *args| if file == __FILE__ @@ -2229,7 +2323,7 @@ CODE _c = a + b end - def check_with_events *trace_events + def check_with_events(trace_point_events, expected_events = trace_point_events) all_events = [[:call, :method_for_enable_target1], [:line, :method_for_enable_target1], [:line, :method_for_enable_target1], @@ -2251,7 +2345,7 @@ CODE [:return, :method_for_enable_target1], ] events = [] - TracePoint.new(*trace_events) do |tp| + TracePoint.new(*trace_point_events) do |tp| next unless target_thread? events << [tp.event, tp.method_id] end.enable(target: method(:method_for_enable_target1)) do @@ -2259,15 +2353,22 @@ CODE method_for_enable_target2 method_for_enable_target1 end - assert_equal all_events.find_all{|(ev)| trace_events.include? ev}, events + + assert_equal all_events.keep_if { |(ev)| expected_events.include? ev }, events end def test_tracepoint_enable_target - check_with_events :line - check_with_events :call, :return - check_with_events :line, :call, :return - check_with_events :call, :return, :b_call, :b_return - check_with_events :line, :call, :return, :b_call, :b_return + check_with_events([:line]) + check_with_events([:call, :return]) + check_with_events([:line, :call, :return]) + check_with_events([:call, :return, :b_call, :b_return]) + check_with_events([:line, :call, :return, :b_call, :b_return]) + + # No arguments passed into TracePoint.new enables all ISEQ_TRACE_EVENTS + check_with_events([], [:line, :class, :end, :call, :return, :c_call, :c_return, :b_call, :b_return, :rescue]) + + # Raise event should be ignored + check_with_events([:line, :raise]) end def test_tracepoint_nested_enabled_with_target @@ -2406,6 +2507,18 @@ CODE assert_equal [:tp1, 1, 2, :tp2, 3], events end + def test_multiple_enable + ary = [] + trace = TracePoint.new(:call) do |tp| + ary << tp.method_id + end + trace.enable + trace.enable + foo + trace.disable + assert_equal(1, ary.count(:foo), '[Bug #19114]') + end + def test_multiple_tracepoints_same_bmethod events = [] tp1 = TracePoint.new(:return) do |tp| @@ -2611,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 @@ -2725,4 +2838,329 @@ CODE Foo.foo RUBY end + + def helper_cant_rescue + begin + raise SyntaxError + rescue + cant_rescue + end + end + + def test_tp_rescue + lines = [] + TracePoint.new(:line){|tp| + next unless target_thread? + lines << tp.lineno + }.enable{ + begin + helper_cant_rescue + rescue SyntaxError + end + } + _call_line = lines.shift + _raise_line = lines.shift + assert_equal [], lines + end + + def helper_can_rescue + begin + raise __LINE__.to_s + rescue SyntaxError + :ng + rescue + :ok + end + end + + def helper_can_rescue_empty_body + begin + raise __LINE__.to_s + rescue SyntaxError + :ng + rescue + end + end + + def test_tp_rescue_event + lines = [] + TracePoint.new(:rescue){|tp| + next unless target_thread? + lines << [tp.lineno, tp.raised_exception] + }.enable{ + helper_can_rescue + } + + line, err, = lines.pop + assert_equal [], lines + assert err.kind_of?(RuntimeError) + assert_equal err.message.to_i + 4, line + + lines = [] + TracePoint.new(:rescue){|tp| + next unless target_thread? + lines << [tp.lineno, tp.raised_exception] + }.enable{ + helper_can_rescue_empty_body + } + + line, err, = lines.pop + assert_equal [], lines + assert err.kind_of?(RuntimeError) + assert_equal err.message.to_i + 3, line + end + + def test_tracepoint_thread_begin + target_thread = nil + + trace = TracePoint.new(:thread_begin) do |tp| + target_thread = tp.self + end + + trace.enable(target_thread: nil) do + Thread.new{}.join + end + + assert_kind_of(Thread, target_thread) + end + + def test_tracepoint_thread_end + target_thread = nil + + trace = TracePoint.new(:thread_end) do |tp| + target_thread = tp.self + end + + trace.enable(target_thread: nil) do + Thread.new{}.join + end + + assert_kind_of(Thread, target_thread) + end + + def test_tracepoint_thread_end_with_exception + target_thread = nil + + trace = TracePoint.new(:thread_end) do |tp| + target_thread = tp.self + end + + trace.enable(target_thread: nil) do + thread = Thread.new do + Thread.current.report_on_exception = false + raise + end + + # Ignore the exception raised by the thread: + thread.join rescue nil + end + + 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 b1a2ba2f1b..67e2c543a3 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -1,8 +1,24 @@ # frozen_string_literal: false require 'test/unit' +require 'objspace' +require 'json' +require 'securerandom' # These test the functionality of object shapes class TestShapes < Test::Unit::TestCase + MANY_IVS = 80 + + class IVOrder + def expected_ivs + %w{ @a @b @c @d @e @f @g @h @i @j @k } + end + + def set_ivs + expected_ivs.each { instance_variable_set(_1, 1) } + self + end + end + class ShapeOrder def initialize @b = :b # 5 => 6 @@ -17,6 +33,14 @@ class TestShapes < Test::Unit::TestCase end end + class OrderedAlloc + def add_ivars + 10.times do |i| + instance_variable_set("@foo" + i.to_s, 0) + end + end + end + class Example def initialize @a = 1 @@ -69,15 +93,28 @@ 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 + (RubyVM::Shape::SHAPE_MAX_VARIATIONS + 1).times { + IVOrder.new.instance_variable_set("@a#{_1}", 1) + } + + obj = IVOrder.new + iv_list = obj.set_ivs.instance_variables + assert_equal obj.expected_ivs, iv_list.map(&:to_s) end def test_too_complex @@ -88,33 +125,48 @@ class TestShapes < Test::Unit::TestCase assert_predicate RubyVM::Shape.of(tc), :too_complex? end + def test_ordered_alloc_is_not_complex + 5.times { OrderedAlloc.new.add_ivars } + obj = JSON.parse(ObjectSpace.dump(OrderedAlloc)) + assert_operator obj["variation_count"], :<, RubyVM::Shape::SHAPE_MAX_VARIATIONS + end + def test_too_many_ivs_on_obj - obj = Object.new + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class Hi; end - (RubyVM::Shape::SHAPE_MAX_NUM_IVS + 1).times do - obj.instance_variable_set(:"@a#{_1}", 1) - end + RubyVM::Shape.exhaust_shapes(2) - assert_predicate RubyVM::Shape.of(obj), :too_complex? + obj = Hi.new + obj.instance_variable_set(:@b, 1) + obj.instance_variable_set(:@c, 1) + obj.instance_variable_set(:@d, 1) + + assert_predicate RubyVM::Shape.of(obj), :too_complex? + end; end def test_too_many_ivs_on_class obj = Class.new - (RubyVM::Shape::SHAPE_MAX_NUM_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 obj = Class.new - (RubyVM::Shape::SHAPE_MAX_NUM_IVS + 2).times do + (MANY_IVS + 2).times do obj.instance_variable_set(:"@a#{_1}", 1) end - (RubyVM::Shape::SHAPE_MAX_NUM_IVS + 2).times do + (MANY_IVS + 2).times do obj.remove_instance_variable(:"@a#{_1}") end @@ -124,16 +176,416 @@ class TestShapes < Test::Unit::TestCase def test_removing_when_too_many_ivs_on_module obj = Module.new - (RubyVM::Shape::SHAPE_MAX_NUM_IVS + 2).times do + (MANY_IVS + 2).times do obj.instance_variable_set(:"@a#{_1}", 1) end - (RubyVM::Shape::SHAPE_MAX_NUM_IVS + 2).times do + (MANY_IVS + 2).times do obj.remove_instance_variable(:"@a#{_1}") end assert_empty obj.instance_variables end + def test_too_complex_geniv + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class TooComplex < Hash + attr_reader :very_unique + end + + RubyVM::Shape.exhaust_shapes + + (RubyVM::Shape::SHAPE_MAX_VARIATIONS * 2).times do + TooComplex.new.instance_variable_set(:"@unique_#{_1}", 1) + end + + tc = TooComplex.new + tc.instance_variable_set(:@very_unique, 3) + tc.instance_variable_set(:@very_unique2, 4) + assert_equal 3, tc.instance_variable_get(:@very_unique) + assert_equal 4, tc.instance_variable_get(:@very_unique2) + + assert_equal [:@very_unique, :@very_unique2], tc.instance_variables + end; + end + + def test_use_all_shapes_then_freeze + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class Hi; end + RubyVM::Shape.exhaust_shapes(3) + + obj = Hi.new + i = 0 + while RubyVM::Shape.shapes_available > 0 + obj.instance_variable_set(:"@b#{i}", 1) + i += 1 + end + obj.freeze + + assert obj.frozen? + end; + end + + def test_run_out_of_shape_for_object + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class A + def initialize + @a = 1 + end + end + RubyVM::Shape.exhaust_shapes + + A.new + end; + end + + def test_run_out_of_shape_for_class_ivar + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + RubyVM::Shape.exhaust_shapes + + c = Class.new + c.instance_variable_set(:@a, 1) + assert_equal(1, c.instance_variable_get(:@a)) + + c.remove_instance_variable(:@a) + assert_nil(c.instance_variable_get(:@a)) + + assert_raise(NameError) do + c.remove_instance_variable(:@a) + end + end; + end + + def test_evacuate_class_ivar_and_compaction + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + count = 20 + + c = Class.new + count.times do |ivar| + c.instance_variable_set("@i#{ivar}", "ivar-#{ivar}") + end + + RubyVM::Shape.exhaust_shapes + + GC.auto_compact = true + GC.stress = true + # Cause evacuation + c.instance_variable_set(:@a, o = Object.new) + assert_equal(o, c.instance_variable_get(:@a)) + GC.stress = false + + count.times do |ivar| + assert_equal "ivar-#{ivar}", c.instance_variable_get("@i#{ivar}") + end + end; + end + + def test_evacuate_generic_ivar_and_compaction + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + count = 20 + + c = Hash.new + count.times do |ivar| + c.instance_variable_set("@i#{ivar}", "ivar-#{ivar}") + end + + RubyVM::Shape.exhaust_shapes + + GC.auto_compact = true + GC.stress = true + + # Cause evacuation + c.instance_variable_set(:@a, o = Object.new) + assert_equal(o, c.instance_variable_get(:@a)) + + GC.stress = false + + count.times do |ivar| + assert_equal "ivar-#{ivar}", c.instance_variable_get("@i#{ivar}") + end + end; + end + + def test_evacuate_object_ivar_and_compaction + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + count = 20 + + c = Object.new + count.times do |ivar| + c.instance_variable_set("@i#{ivar}", "ivar-#{ivar}") + end + + RubyVM::Shape.exhaust_shapes + + GC.auto_compact = true + GC.stress = true + + # Cause evacuation + c.instance_variable_set(:@a, o = Object.new) + assert_equal(o, c.instance_variable_get(:@a)) + + GC.stress = false + + count.times do |ivar| + assert_equal "ivar-#{ivar}", c.instance_variable_get("@i#{ivar}") + end + end; + end + + def test_gc_stress_during_evacuate_generic_ivar + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + [].instance_variable_set(:@a, 1) + + RubyVM::Shape.exhaust_shapes + + ary = 10.times.map { [] } + + GC.stress = true + ary.each do |o| + o.instance_variable_set(:@a, 1) + o.instance_variable_set(:@b, 1) + end + end; + end + + def test_run_out_of_shape_for_module_ivar + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + RubyVM::Shape.exhaust_shapes + + module Foo + @a = 1 + @b = 2 + assert_equal 1, @a + assert_equal 2, @b + end + end; + end + + def test_run_out_of_shape_for_class_cvar + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + RubyVM::Shape.exhaust_shapes + + c = Class.new + + c.class_variable_set(:@@a, 1) + assert_equal(1, c.class_variable_get(:@@a)) + + c.class_eval { remove_class_variable(:@@a) } + assert_false(c.class_variable_defined?(:@@a)) + + assert_raise(NameError) do + c.class_eval { remove_class_variable(:@@a) } + end + end; + end + + def test_run_out_of_shape_generic_instance_variable_set + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class TooComplex < Hash + end + + RubyVM::Shape.exhaust_shapes + + tc = TooComplex.new + tc.instance_variable_set(:@a, 1) + tc.instance_variable_set(:@b, 2) + + tc.remove_instance_variable(:@a) + assert_nil(tc.instance_variable_get(:@a)) + + assert_raise(NameError) do + tc.remove_instance_variable(:@a) + end + end; + end + + def test_run_out_of_shape_generic_ivar_set + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class Hi < String + def initialize + 8.times do |i| + instance_variable_set("@ivar_#{i}", i) + end + end + + def transition + @hi_transition ||= 1 + end + end + + a = Hi.new + + # Try to run out of shapes + RubyVM::Shape.exhaust_shapes + + assert_equal 1, a.transition + assert_equal 1, a.transition + end; + end + + def test_run_out_of_shape_instance_variable_defined + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class A + attr_reader :a, :b, :c, :d + def initialize + @a = @b = @c = @d = 1 + end + end + + RubyVM::Shape.exhaust_shapes + + a = A.new + assert_equal true, a.instance_variable_defined?(:@a) + end; + end + + def test_run_out_of_shape_instance_variable_defined_on_module + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + RubyVM::Shape.exhaust_shapes + + module A + @a = @b = @c = @d = 1 + end + + assert_equal true, A.instance_variable_defined?(:@a) + end; + end + + def test_run_out_of_shape_during_remove_instance_variable + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + o = Object.new + 10.times { |i| o.instance_variable_set(:"@a#{i}", i) } + + RubyVM::Shape.exhaust_shapes + + o.remove_instance_variable(:@a0) + (1...10).each do |i| + assert_equal(i, o.instance_variable_get(:"@a#{i}")) + end + end; + end + + def test_run_out_of_shape_remove_instance_variable + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class A + attr_reader :a, :b, :c, :d + def initialize + @a = @b = @c = @d = 1 + end + end + + a = A.new + + RubyVM::Shape.exhaust_shapes + + a.remove_instance_variable(:@b) + assert_nil a.b + + a.remove_instance_variable(:@a) + assert_nil a.a + + a.remove_instance_variable(:@c) + assert_nil a.c + + assert_equal 1, a.d + end; + end + + def test_run_out_of_shape_rb_obj_copy_ivar + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class A + def initialize + init # Avoid right sizing + end + + def init + @a = @b = @c = @d = @e = @f = 1 + end + end + + a = A.new + + RubyVM::Shape.exhaust_shapes + + a.dup + end; + end + + def test_evacuate_generic_ivar_memory_leak + assert_no_memory_leak([], "#{<<~'begin;'}", "#{<<~'end;'}", rss: true) + o = [] + o.instance_variable_set(:@a, 1) + + RubyVM::Shape.exhaust_shapes + + ary = 1_000_000.times.map { [] } + begin; + ary.each do |o| + o.instance_variable_set(:@a, 1) + o.instance_variable_set(:@b, 1) + end + ary.clear + ary = nil + GC.start + end; + end + + def test_use_all_shapes_module + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class Hi; end + + RubyVM::Shape.exhaust_shapes(2) + + obj = Module.new + 3.times do + obj.instance_variable_set(:"@a#{_1}", _1) + end + + ivs = 3.times.map do + obj.instance_variable_get(:"@a#{_1}") + end + + assert_equal [0, 1, 2], ivs + end; + end + + def test_complex_freeze_after_clone + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class Hi; end + + RubyVM::Shape.exhaust_shapes(2) + + obj = Object.new + i = 0 + while RubyVM::Shape.shapes_available > 0 + obj.instance_variable_set(:"@b#{i}", i) + i += 1 + end + + v = obj.clone(freeze: true) + assert_predicate v, :frozen? + assert_equal 0, v.instance_variable_get(:@b0) + end; + end + def test_too_complex_ractor assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; @@ -151,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 @@ -177,6 +629,133 @@ 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; + $VERBOSE = nil + + RubyVM::Shape.exhaust_shapes + + r = Ractor.new do + o = Object.new + o.instance_variable_set(:@a, "hello") + o + end + + o = r.value + assert_equal "hello", o.instance_variable_get(:@a) + end; + end + + def test_too_complex_generic_ivar_ractor_share + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + $VERBOSE = nil + + RubyVM::Shape.exhaust_shapes + + r = Ractor.new do + o = [] + o.instance_variable_set(:@a, "hello") + o + end + + o = r.value + assert_equal "hello", o.instance_variable_get(:@a) + end; + end + def test_read_iv_after_complex ensure_complex @@ -240,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 @@ -257,6 +869,86 @@ class TestShapes < Test::Unit::TestCase assert_nil tc.a3 end + def test_remove_instance_variable + ivars_count = 5 + object = Object.new + ivars_count.times do |i| + object.instance_variable_set("@ivar_#{i}", i) + end + + ivars = ivars_count.times.map do |i| + object.instance_variable_get("@ivar_#{i}") + end + assert_equal [0, 1, 2, 3, 4], ivars + + object.remove_instance_variable(:@ivar_2) + + ivars = ivars_count.times.map do |i| + object.instance_variable_get("@ivar_#{i}") + end + assert_equal [0, 1, nil, 3, 4], ivars + end + + def test_remove_instance_variable_when_out_of_shapes + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + ivars_count = 5 + object = Object.new + ivars_count.times do |i| + object.instance_variable_set("@ivar_#{i}", i) + end + + ivars = ivars_count.times.map do |i| + object.instance_variable_get("@ivar_#{i}") + end + assert_equal [0, 1, 2, 3, 4], ivars + + RubyVM::Shape.exhaust_shapes + + object.remove_instance_variable(:@ivar_2) + + ivars = ivars_count.times.map do |i| + object.instance_variable_get("@ivar_#{i}") + end + assert_equal [0, 1, nil, 3, 4], ivars + end; + end + + def test_remove_instance_variable_capacity_transition + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + + # 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 + + # b transitions in capacity + b = Class.new.new + (initial_capacity + 1).times do |i| + b.instance_variable_set(:"@ivar#{i}", i) + end + + assert_operator(RubyVM::Shape.of(a).capacity, :<, RubyVM::Shape.of(b).capacity) + + # b will now have the same tree as a + b.remove_instance_variable(:@ivar0) + + a.instance_variable_set(:@foo, 1) + a.instance_variable_set(:@bar, 1) + + # Check that there is no heap corruption + GC.verify_internal_consistency + end; + end + def test_freeze_after_complex ensure_complex @@ -265,6 +957,8 @@ class TestShapes < Test::Unit::TestCase assert_predicate RubyVM::Shape.of(tc), :too_complex? tc.freeze assert_raise(FrozenError) { tc.a3_m } + # doesn't transition to frozen shape in this case + assert_predicate RubyVM::Shape.of(tc), :too_complex? end def test_read_undefined_iv_after_complex @@ -274,6 +968,7 @@ class TestShapes < Test::Unit::TestCase tc.send("a#{RubyVM::Shape::SHAPE_MAX_VARIATIONS}_m") assert_predicate RubyVM::Shape.of(tc), :too_complex? assert_equal nil, tc.iv_not_defined + assert_predicate RubyVM::Shape.of(tc), :too_complex? end def test_shape_order @@ -290,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) @@ -306,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 @@ -324,7 +1019,10 @@ class TestShapes < Test::Unit::TestCase class TestObject; end def test_new_obj_has_t_object_shape - assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of(TestObject.new).parent) + obj = TestObject.new + shape = RubyVM::Shape.of(obj) + assert_equal RubyVM::Shape::SHAPE_ROOT, shape.type + assert_nil shape.parent end def test_str_has_root_shape @@ -335,20 +1033,32 @@ class TestShapes < Test::Unit::TestCase assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of([])) end - def test_hash_has_root_shape - 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) + 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_nil_has_special_const_shape_id - assert_equal(RubyVM::Shape::SPECIAL_CONST_SHAPE_ID, RubyVM::Shape.of(nil).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 - omit "Failing with RJIT for some reason" if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? obj = Example.new shape = RubyVM::Shape.of(obj) refute_equal(RubyVM::Shape.root_shape, shape) @@ -356,11 +1066,10 @@ 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 - shape = shape.parent - assert_equal(RubyVM::Shape.root_shape.id, shape.id) - assert_equal(obj.instance_variable_get(:@a), 1) + assert_equal(1, obj.instance_variable_get(:@a)) end def test_different_objects_make_same_transition @@ -377,13 +1086,27 @@ class TestShapes < Test::Unit::TestCase assert_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) end + def test_duplicating_too_complex_objects_memory_leak + assert_no_memory_leak([], "#{<<~'begin;'}", "#{<<~'end;'}", "[Bug #20162]", rss: true) + RubyVM::Shape.exhaust_shapes + + o = Object.new + o.instance_variable_set(:@a, 0) + begin; + 1_000_000.times do + o.dup + end + end; + end + 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 @@ -400,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 @@ -411,6 +1135,14 @@ class TestShapes < Test::Unit::TestCase assert_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) end + def test_cloning_with_freeze_option + obj = Object.new + obj2 = obj.clone(freeze: true) + assert_predicate(obj2, :frozen?) + refute_shape_equal(RubyVM::Shape.of(obj), RubyVM::Shape.of(obj2)) + assert_predicate(RubyVM::Shape.of(obj2), :shape_frozen?) + end + def test_freezing_and_cloning_object_with_ivars obj = Example.new.freeze obj2 = obj.clone(freeze: true) @@ -420,7 +1152,7 @@ class TestShapes < Test::Unit::TestCase end def test_freezing_and_cloning_string - str = "str".freeze + str = ("str" + "str").freeze str2 = str.clone(freeze: true) assert_predicate(str2, :frozen?) assert_shape_equal(RubyVM::Shape.of(str), RubyVM::Shape.of(str2)) @@ -451,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 7877a35129..091a66d5da 100644 --- a/test/ruby/test_signal.rb +++ b/test/ruby/test_signal.rb @@ -310,22 +310,30 @@ class TestSignal < Test::Unit::TestCase end def test_self_stop - assert_ruby_status([], <<-'end;') - begin - fork{ - sleep 1 - Process.kill(:CONT, Process.ppid) - } - Process.kill(:STOP, Process.pid) - rescue NotImplementedError - # ok - end - end; + omit unless Process.respond_to?(:fork) + omit unless defined?(Process::WUNTRACED) + + # Make a process that stops itself + child_pid = fork do + Process.kill(:STOP, Process.pid) + end + + # The parent should be notified about the stop + _, status = Process.waitpid2(child_pid, Process::WUNTRACED) + 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_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 61002b8b18..7ef962db4a 100644 --- a/test/ruby/test_sleep.rb +++ b/test/ruby/test_sleep.rb @@ -1,17 +1,34 @@ # frozen_string_literal: false require 'test/unit' require 'etc' +require 'timeout' class TestSleep < Test::Unit::TestCase def test_sleep_5sec - GC.disable - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - sleep 5 - slept = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start - bottom = 5.0 - assert_operator(slept, :>=, bottom) - assert_operator(slept, :<=, 6.0, "[ruby-core:18015]: longer than expected") - ensure - GC.enable + EnvUtil.without_gc do + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + sleep 5 + slept = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + bottom = 5.0 + assert_operator(slept, :>=, bottom) + 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_sprintf.rb b/test/ruby/test_sprintf.rb index c453ecd350..1c7e89c265 100644 --- a/test/ruby/test_sprintf.rb +++ b/test/ruby/test_sprintf.rb @@ -227,6 +227,10 @@ class TestSprintf < Test::Unit::TestCase bug11766 = '[ruby-core:71806] [Bug #11766]' assert_equal("x"*10+" 1.0", sprintf("x"*10+"%8.1f", 1r), bug11766) + + require 'rbconfig/sizeof' + fmin, fmax = RbConfig::LIMITS.values_at("FIXNUM_MIN", "FIXNUM_MAX") + assert_match(/\A-\d+\.\d+\z/, sprintf("%f", Rational(fmin, fmax))) end def test_rational_precision @@ -235,7 +239,7 @@ class TestSprintf < Test::Unit::TestCase def test_hash options = {:capture=>/\d+/} - assert_equal("with options {:capture=>/\\d+/}", sprintf("with options %p" % options)) + assert_equal("with options #{options.inspect}", sprintf("with options %p" % options)) end def test_inspect @@ -266,8 +270,8 @@ class TestSprintf < Test::Unit::TestCase # Specifying the precision multiple times with negative star arguments: assert_raise(ArgumentError, "[ruby-core:11570]") {sprintf("%.*.*.*.*f", -1, -1, -1, 5, 1)} - # Null bytes after percent signs are removed: - assert_equal("%\0x hello", sprintf("%\0x hello"), "[ruby-core:11571]") + assert_raise(ArgumentError) {sprintf("%\0x hello")} + assert_raise(ArgumentError) {sprintf("%\nx hello")} assert_raise(ArgumentError, "[ruby-core:11573]") {sprintf("%.25555555555555555555555555555555555555s", "hello")} @@ -279,10 +283,9 @@ class TestSprintf < Test::Unit::TestCase assert_raise_with_message(ArgumentError, /unnumbered\(1\) mixed with numbered/) { sprintf("%1$*d", 3) } assert_raise_with_message(ArgumentError, /unnumbered\(1\) mixed with numbered/) { sprintf("%1$.*d", 3) } - verbose, $VERBOSE = $VERBOSE, nil - assert_nothing_raised { sprintf("", 1) } - ensure - $VERBOSE = verbose + assert_warning(/too many arguments/) do + sprintf("", 1) + end end def test_float @@ -543,4 +546,12 @@ class TestSprintf < Test::Unit::TestCase sprintf("%*s", RbConfig::LIMITS["INT_MIN"], "") end end + + def test_binary_format_coderange + 1.upto(500) do |i| + str = sprintf("%*s".b, i, "\xe2".b) + refute_predicate str, :ascii_only? + assert_equal i, str.bytesize + end + end end diff --git a/test/ruby/test_stack.rb b/test/ruby/test_stack.rb index 763aeb6bc2..8a78848322 100644 --- a/test/ruby/test_stack.rb +++ b/test/ruby/test_stack.rb @@ -18,7 +18,6 @@ class TestStack < Test::Unit::TestCase env = {} env['RUBY_FIBER_VM_STACK_SIZE'] = vm_stack_size.to_s if vm_stack_size env['RUBY_FIBER_MACHINE_STACK_SIZE'] = machine_stack_size.to_s if machine_stack_size - env['ASAN_OPTIONS'] = ENV['ASAN_OPTIONS'] if ENV['ASAN_OPTIONS'] stdout, stderr, status = EnvUtil.invoke_ruby([env, '-e', script], '', true, true, timeout: 30) assert(!status.signaled?, FailDesc[status, nil, stderr]) diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index a916c8049e..2458d38ef4 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -9,9 +9,6 @@ class TestString < Test::Unit::TestCase def initialize(*args) @cls = String - @aref_re_nth = true - @aref_re_silent = false - @aref_slicebang_silent = true super end @@ -80,6 +77,13 @@ class TestString < Test::Unit::TestCase assert_equal("mystring", str.__send__(:initialize, "mystring", capacity: 1000)) str = S("mystring") assert_equal("mystring", str.__send__(:initialize, str, capacity: 1000)) + + if @cls == String + 100.times { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa". + __send__(:initialize, capacity: -1) + } + end end def test_initialize_shared @@ -100,7 +104,7 @@ class TestString < Test::Unit::TestCase return unless @cls == String assert_no_memory_leak([], <<-PREP, <<-CODE, rss: true) -code = proc {('x'*100000).__send__(:initialize, '')} +code = proc {('x'*100_000).__send__(:initialize, '')} 1_000.times(&code) PREP 100_000.times(&code) @@ -112,7 +116,7 @@ CODE return unless @cls == String assert_no_memory_leak([], <<-PREP, <<-CODE, rss: true) -code = proc {0.to_s.__send__(:initialize, capacity: 10000)} +code = proc {0.to_s.__send__(:initialize, capacity: 100_000)} 1_000.times(&code) PREP 100_000.times(&code) @@ -146,14 +150,12 @@ CODE assert_equal(nil, S("FooBar")[S("xyzzy")]) assert_equal(nil, S("FooBar")[S("plugh")]) - if @aref_re_nth - assert_equal(S("Foo"), S("FooBar")[/([A-Z]..)([A-Z]..)/, 1]) - assert_equal(S("Bar"), S("FooBar")[/([A-Z]..)([A-Z]..)/, 2]) - assert_equal(nil, S("FooBar")[/([A-Z]..)([A-Z]..)/, 3]) - assert_equal(S("Bar"), S("FooBar")[/([A-Z]..)([A-Z]..)/, -1]) - assert_equal(S("Foo"), S("FooBar")[/([A-Z]..)([A-Z]..)/, -2]) - assert_equal(nil, S("FooBar")[/([A-Z]..)([A-Z]..)/, -3]) - end + assert_equal(S("Foo"), S("FooBar")[/([A-Z]..)([A-Z]..)/, 1]) + assert_equal(S("Bar"), S("FooBar")[/([A-Z]..)([A-Z]..)/, 2]) + assert_equal(nil, S("FooBar")[/([A-Z]..)([A-Z]..)/, 3]) + assert_equal(S("Bar"), S("FooBar")[/([A-Z]..)([A-Z]..)/, -1]) + assert_equal(S("Foo"), S("FooBar")[/([A-Z]..)([A-Z]..)/, -2]) + assert_equal(nil, S("FooBar")[/([A-Z]..)([A-Z]..)/, -3]) o = Object.new def o.to_int; 2; end @@ -162,6 +164,15 @@ CODE assert_raise(ArgumentError) { "foo"[] } end + def test_AREF_underflow + require "rbconfig/sizeof" + assert_equal(nil, S("\u{3042 3044 3046}")[RbConfig::LIMITS["LONG_MIN"], 1]) + end + + def test_AREF_invalid_encoding + assert_equal(S("\x80"), S("A"*39+"\x80")[-1, 1]) + end + def test_ASET # '[]=' s = S("FooBar") s[0] = S('A') @@ -199,24 +210,18 @@ CODE assert_equal(S("BarBar"), s) s[/..r$/] = S("Foo") assert_equal(S("BarFoo"), s) - if @aref_re_silent - s[/xyzzy/] = S("None") - assert_equal(S("BarFoo"), s) - else - assert_raise(IndexError) { s[/xyzzy/] = S("None") } - end - if @aref_re_nth - s[/([A-Z]..)([A-Z]..)/, 1] = S("Foo") - assert_equal(S("FooFoo"), s) - s[/([A-Z]..)([A-Z]..)/, 2] = S("Bar") - assert_equal(S("FooBar"), s) - assert_raise(IndexError) { s[/([A-Z]..)([A-Z]..)/, 3] = "None" } - s[/([A-Z]..)([A-Z]..)/, -1] = S("Foo") - assert_equal(S("FooFoo"), s) - s[/([A-Z]..)([A-Z]..)/, -2] = S("Bar") - assert_equal(S("BarFoo"), s) - assert_raise(IndexError) { s[/([A-Z]..)([A-Z]..)/, -3] = "None" } - end + assert_raise(IndexError) { s[/xyzzy/] = S("None") } + + s[/([A-Z]..)([A-Z]..)/, 1] = S("Foo") + assert_equal(S("FooFoo"), s) + s[/([A-Z]..)([A-Z]..)/, 2] = S("Bar") + assert_equal(S("FooBar"), s) + assert_raise(IndexError) { s[/([A-Z]..)([A-Z]..)/, 3] = "None" } + s[/([A-Z]..)([A-Z]..)/, -1] = S("Foo") + assert_equal(S("FooFoo"), s) + s[/([A-Z]..)([A-Z]..)/, -2] = S("Bar") + assert_equal(S("BarFoo"), s) + assert_raise(IndexError) { s[/([A-Z]..)([A-Z]..)/, -3] = "None" } s = S("FooBar") s[S("Foo")] = S("Bar") @@ -301,6 +306,9 @@ CODE assert_raise(RangeError, bug) {S("a".force_encoding(Encoding::UTF_8)) << -1} assert_raise(RangeError, bug) {S("a".force_encoding(Encoding::UTF_8)) << 0x81308130} assert_nothing_raised {S("a".force_encoding(Encoding::GB18030)) << 0x81308130} + + s = "\x95".force_encoding(Encoding::SJIS).tap(&:valid_encoding?) + assert_predicate(s << 0x5c, :valid_encoding?) end def test_MATCH # '=~' @@ -587,6 +595,8 @@ CODE assert_equal("foo", s.chomp!("\n")) s = "foo\r" assert_equal("foo", s.chomp!("\n")) + + assert_raise(ArgumentError) {String.new.chomp!("", "")} ensure $/ = save $VERBOSE = verbose @@ -661,8 +671,8 @@ CODE assert_equal(Encoding::UTF_8, "#{s}x".encoding) end - def test_string_interpolations_across_size_pools_get_embedded - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + def test_string_interpolations_across_heaps_get_embedded + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 require 'objspace' base_slot_size = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] @@ -862,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 } @@ -896,6 +910,18 @@ CODE } end + def test_undump_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + a = S("Test") << 1 << 2 << 3 << 9 << 13 << 10 + EnvUtil.under_gc_compact_stress do + assert_equal(a, S('"Test\\x01\\x02\\x03\\t\\r\\n"').undump) + end + + EnvUtil.under_gc_compact_stress do + assert_equal(S("\u{ABCDE 10ABCD}"), S('"\\u{ABCDE 10ABCD}"').undump) + end + end + def test_dup for frozen in [ false, true ] a = S("hello") @@ -1095,6 +1121,22 @@ CODE assert_equal("C", res[2]) end + def test_grapheme_clusters_memory_leak + assert_no_memory_leak([], "", "#{<<~"begin;"}\n#{<<~'end;'}", "[Bug #todo]", rss: true) + begin; + str = "hello world".encode(Encoding::UTF_32LE) + + 10_000.times do + str.grapheme_clusters + end + end; + end + + def test_byteslice_grapheme_clusters + string = "안녕" + assert_equal(["안"], string.byteslice(0,4).grapheme_clusters) + end + def test_each_line verbose, $VERBOSE = $VERBOSE, nil @@ -1249,6 +1291,11 @@ CODE assert_raise(ArgumentError) { S("foo").gsub } end + def test_gsub_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress { assert_equal(S("h<e>ll<o>"), S("hello").gsub(/([aeiou])/, S('<\1>'))) } + end + def test_gsub_encoding a = S("hello world") a.force_encoding Encoding::UTF_8 @@ -1292,6 +1339,15 @@ CODE assert_nil(a.sub!(S('X'), S('Y'))) end + def test_gsub_bang_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + a = S("hello") + a.gsub!(/([aeiou])/, S('<\1>')) + assert_equal(S("h<e>ll<o>"), a) + end + end + def test_sub_hash assert_equal('azc', S('abc').sub(/b/, "b" => "z")) assert_equal('ac', S('abc').sub(/b/, {})) @@ -1319,6 +1375,9 @@ CODE assert_not_equal(S("a").hash, S("a\0").hash, bug4104) bug9172 = '[ruby-core:58658] [Bug #9172]' assert_not_equal(S("sub-setter").hash, S("discover").hash, bug9172) + assert_equal(S("").hash, S("".encode(Encoding::UTF_32BE)).hash) + h1, h2 = ["\x80", "\x81"].map {|c| c.b.hash ^ c.hash} + assert_not_equal(h1, h2) end def test_hex @@ -1339,54 +1398,54 @@ CODE end def test_index - assert_equal(0, S("hello").index(?h)) - assert_equal(1, S("hello").index(S("ell"))) - assert_equal(2, S("hello").index(/ll./)) + assert_index(0, S("hello"), ?h) + assert_index(1, S("hello"), S("ell")) + assert_index(2, S("hello"), /ll./) - assert_equal(3, S("hello").index(?l, 3)) - assert_equal(3, S("hello").index(S("l"), 3)) - assert_equal(3, S("hello").index(/l./, 3)) + assert_index(3, S("hello"), ?l, 3) + assert_index(3, S("hello"), S("l"), 3) + assert_index(3, S("hello"), /l./, 3) - assert_nil(S("hello").index(?z, 3)) - assert_nil(S("hello").index(S("z"), 3)) - assert_nil(S("hello").index(/z./, 3)) + assert_index(nil, S("hello"), ?z, 3) + assert_index(nil, S("hello"), S("z"), 3) + assert_index(nil, S("hello"), /z./, 3) - assert_nil(S("hello").index(?z)) - assert_nil(S("hello").index(S("z"))) - assert_nil(S("hello").index(/z./)) + assert_index(nil, S("hello"), ?z) + assert_index(nil, S("hello"), S("z")) + assert_index(nil, S("hello"), /z./) - assert_equal(0, S("").index(S(""))) - assert_equal(0, S("").index(//)) - assert_nil(S("").index(S("hello"))) - assert_nil(S("").index(/hello/)) - assert_equal(0, S("hello").index(S(""))) - assert_equal(0, S("hello").index(//)) + assert_index(0, S(""), S("")) + assert_index(0, S(""), //) + assert_index(nil, S(""), S("hello")) + assert_index(nil, S(""), /hello/) + assert_index(0, S("hello"), S("")) + assert_index(0, S("hello"), //) s = S("long") * 1000 << "x" - assert_nil(s.index(S("y"))) - assert_equal(4 * 1000, s.index(S("x"))) + assert_index(nil, s, S("y")) + assert_index(4 * 1000, s, S("x")) s << "yx" - assert_equal(4 * 1000, s.index(S("x"))) - assert_equal(4 * 1000, s.index(S("xyx"))) + assert_index(4 * 1000, s, S("x")) + assert_index(4 * 1000, s, S("xyx")) o = Object.new def o.to_str; "bar"; end - assert_equal(3, S("foobarbarbaz").index(o)) + assert_index(3, S("foobarbarbaz"), o) assert_raise(TypeError) { S("foo").index(Object.new) } - assert_nil(S("foo").index(//, -100)) - assert_nil($~) + assert_index(nil, S("foo"), //, -100) + assert_index(nil, S("foo"), //, 4) - assert_equal(2, S("abcdbce").index(/b\Kc/)) + assert_index(2, S("abcdbce"), /b\Kc/) - assert_equal(0, S("ã“ã‚“ã«ã¡ã¯").index(?ã“)) - assert_equal(1, S("ã“ã‚“ã«ã¡ã¯").index(S("ã‚“ã«ã¡"))) - assert_equal(2, S("ã“ã‚“ã«ã¡ã¯").index(/ã«ã¡./)) + assert_index(0, S("ã“ã‚“ã«ã¡ã¯"), ?ã“) + assert_index(1, S("ã“ã‚“ã«ã¡ã¯"), S("ã‚“ã«ã¡")) + assert_index(2, S("ã“ã‚“ã«ã¡ã¯"), /ã«ã¡./) - assert_equal(0, S("ã«ã‚“ã«ã¡ã¯").index(?ã«, 0)) - assert_equal(2, S("ã«ã‚“ã«ã¡ã¯").index(?ã«, 1)) - assert_equal(2, S("ã«ã‚“ã«ã¡ã¯").index(?ã«, 2)) - assert_nil(S("ã«ã‚“ã«ã¡ã¯").index(?ã«, 3)) + assert_index(0, S("ã«ã‚“ã«ã¡ã¯"), ?ã«, 0) + assert_index(2, S("ã«ã‚“ã«ã¡ã¯"), ?ã«, 1) + assert_index(2, S("ã«ã‚“ã«ã¡ã¯"), ?ã«, 2) + assert_index(nil, S("ã«ã‚“ã«ã¡ã¯"), ?ã«, 3) end def test_insert @@ -1533,57 +1592,57 @@ CODE end def test_rindex - assert_equal(3, S("hello").rindex(?l)) - assert_equal(6, S("ell, hello").rindex(S("ell"))) - assert_equal(7, S("ell, hello").rindex(/ll./)) + assert_rindex(3, S("hello"), ?l) + assert_rindex(6, S("ell, hello"), S("ell")) + assert_rindex(7, S("ell, hello"), /ll./) - assert_equal(3, S("hello,lo").rindex(?l, 3)) - assert_equal(3, S("hello,lo").rindex(S("l"), 3)) - assert_equal(3, S("hello,lo").rindex(/l./, 3)) + assert_rindex(3, S("hello,lo"), ?l, 3) + assert_rindex(3, S("hello,lo"), S("l"), 3) + assert_rindex(3, S("hello,lo"), /l./, 3) - assert_nil(S("hello").rindex(?z, 3)) - assert_nil(S("hello").rindex(S("z"), 3)) - assert_nil(S("hello").rindex(/z./, 3)) + assert_rindex(nil, S("hello"), ?z, 3) + assert_rindex(nil, S("hello"), S("z"), 3) + assert_rindex(nil, S("hello"), /z./, 3) - assert_nil(S("hello").rindex(?z)) - assert_nil(S("hello").rindex(S("z"))) - assert_nil(S("hello").rindex(/z./)) + assert_rindex(nil, S("hello"), ?z) + assert_rindex(nil, S("hello"), S("z")) + assert_rindex(nil, S("hello"), /z./) - assert_equal(5, S("hello").rindex(S(""))) - assert_equal(5, S("hello").rindex(S(""), 5)) - assert_equal(4, S("hello").rindex(S(""), 4)) - assert_equal(0, S("hello").rindex(S(""), 0)) + assert_rindex(5, S("hello"), S("")) + assert_rindex(5, S("hello"), S(""), 5) + assert_rindex(4, S("hello"), S(""), 4) + assert_rindex(0, S("hello"), S(""), 0) o = Object.new def o.to_str; "bar"; end - assert_equal(6, S("foobarbarbaz").rindex(o)) + assert_rindex(6, S("foobarbarbaz"), o) assert_raise(TypeError) { S("foo").rindex(Object.new) } - assert_nil(S("foo").rindex(//, -100)) - assert_nil($~) + assert_rindex(nil, S("foo"), //, -100) - assert_equal(3, S("foo").rindex(//)) - assert_equal([3, 3], $~.offset(0)) + m = assert_rindex(3, S("foo"), //) + assert_equal([3, 3], m.offset(0)) + assert_rindex(3, S("foo"), //, 4) - assert_equal(5, S("abcdbce").rindex(/b\Kc/)) + assert_rindex(5, S("abcdbce"), /b\Kc/) - assert_equal(2, S("ã“ã‚“ã«ã¡ã¯").rindex(?ã«)) - assert_equal(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"))) - assert_equal(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(/ã«ã¡./)) + assert_rindex(2, S("ã“ã‚“ã«ã¡ã¯"), ?ã«) + assert_rindex(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯")) + assert_rindex(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), /ã«ã¡./) - assert_equal(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), 7)) - assert_equal(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), -2)) - assert_equal(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), 6)) - assert_equal(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), -3)) - assert_equal(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), 5)) - assert_equal(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), -4)) - assert_equal(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), 1)) - assert_equal(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").rindex(S("ã«ã¡ã¯"), 0)) + assert_rindex(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), 7) + assert_rindex(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), -2) + assert_rindex(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), 6) + assert_rindex(6, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), -3) + assert_rindex(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), 5) + assert_rindex(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), -4) + assert_rindex(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), 1) + assert_rindex(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), 0) - assert_equal(0, S("ã“ã‚“ã«ã¡ã¯").rindex(S("ã“ã‚“ã«ã¡ã¯"))) - assert_nil(S("ã“ã‚“ã«ã¡").rindex(S("ã“ã‚“ã«ã¡ã¯"))) - assert_nil(S("ã“").rindex(S("ã“ã‚“ã«ã¡ã¯"))) - assert_nil(S("").rindex(S("ã“ã‚“ã«ã¡ã¯"))) + assert_rindex(0, S("ã“ã‚“ã«ã¡ã¯"), S("ã“ã‚“ã«ã¡ã¯")) + assert_rindex(nil, S("ã“ã‚“ã«ã¡"), S("ã“ã‚“ã«ã¡ã¯")) + assert_rindex(nil, S("ã“"), S("ã“ã‚“ã«ã¡ã¯")) + assert_rindex(nil, S(""), S("ã“ã‚“ã«ã¡ã¯")) end def test_rjust @@ -1622,6 +1681,11 @@ CODE assert_equal(%w[1 2 3], S("a1 a2 a3").scan(/a\K./)) end + def test_scan_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress { assert_equal([["1a"], ["2b"], ["3c"]], S("1a2b3c").scan(/(\d.)/)) } + end + def test_scan_segv bug19159 = '[Bug #19159]' assert_nothing_raised(Exception, bug19159) do @@ -1682,20 +1746,11 @@ CODE assert_equal(S("FooBa"), a) a = S("FooBar") - if @aref_slicebang_silent - assert_nil( a.slice!(6) ) - assert_nil( a.slice!(6r) ) - else - assert_raise(IndexError) { a.slice!(6) } - assert_raise(IndexError) { a.slice!(6r) } - end + assert_nil( a.slice!(6) ) + assert_nil( a.slice!(6r) ) assert_equal(S("FooBar"), a) - if @aref_slicebang_silent - assert_nil( a.slice!(-7) ) - else - assert_raise(IndexError) { a.slice!(-7) } - end + assert_nil( a.slice!(-7) ) assert_equal(S("FooBar"), a) a = S("FooBar") @@ -1707,17 +1762,9 @@ CODE assert_equal(S("Foo"), a) a=S("FooBar") - if @aref_slicebang_silent assert_nil(a.slice!(7,2)) # Maybe should be six? - else - assert_raise(IndexError) {a.slice!(7,2)} # Maybe should be six? - end assert_equal(S("FooBar"), a) - if @aref_slicebang_silent assert_nil(a.slice!(-7,10)) - else - assert_raise(IndexError) {a.slice!(-7,10)} - end assert_equal(S("FooBar"), a) a=S("FooBar") @@ -1729,17 +1776,9 @@ CODE assert_equal(S("Foo"), a) a=S("FooBar") - if @aref_slicebang_silent assert_equal(S(""), a.slice!(6..2)) - else - assert_raise(RangeError) {a.slice!(6..2)} - end assert_equal(S("FooBar"), a) - if @aref_slicebang_silent assert_nil(a.slice!(-10..-7)) - else - assert_raise(RangeError) {a.slice!(-10..-7)} - end assert_equal(S("FooBar"), a) a=S("FooBar") @@ -1751,17 +1790,9 @@ CODE assert_equal(S("Foo"), a) a=S("FooBar") - if @aref_slicebang_silent - assert_nil(a.slice!(/xyzzy/)) - else - assert_raise(IndexError) {a.slice!(/xyzzy/)} - end + assert_nil(a.slice!(/xyzzy/)) assert_equal(S("FooBar"), a) - if @aref_slicebang_silent - assert_nil(a.slice!(/plugh/)) - else - assert_raise(IndexError) {a.slice!(/plugh/)} - end + assert_nil(a.slice!(/plugh/)) assert_equal(S("FooBar"), a) a=S("FooBar") @@ -1842,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 @@ -1849,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' @@ -1938,16 +1991,37 @@ CODE assert_send([S("hello"), :start_with?, S("hel")]) assert_not_send([S("hello"), :start_with?, S("el")]) assert_send([S("hello"), :start_with?, S("el"), S("he")]) + assert_send([S("\xFF\xFE"), :start_with?, S("\xFF")]) + assert_send([S("hello\xBE"), :start_with?, S("hello")]) + assert_not_send([S("\u{c4}"), :start_with?, S("\xC3")]) bug5536 = '[ruby-core:40623]' assert_raise(TypeError, bug5536) {S("str").start_with? :not_convertible_to_string} + end + def test_start_with_regexp assert_equal(true, S("hello").start_with?(/hel/)) assert_equal("hel", $&) assert_equal(false, S("hello").start_with?(/el/)) assert_nil($&) end + def test_start_with_timeout_memory_leak + assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~'end;'}", "[Bug #20653]", rss: true) + regex = Regexp.new("^#{"(a*)" * 10_000}x$", timeout: 0.000001) + str = "a" * 1_000_000 + "x" + + code = proc do + str.start_with?(regex) + rescue + end + + 10.times(&code) + begin; + 1_000.times(&code) + end; + end + def test_strip assert_equal(S("x"), S(" x ").strip) assert_equal(S("x"), S(" \n\r\t x \t\r\n\n ").strip) @@ -1979,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>'))) @@ -2044,6 +2229,16 @@ CODE } end + def test_sub_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + m = /&(?<foo>.*?);/.match(S("aaa & yyy")) + assert_equal("amp", m["foo"]) + + assert_equal("aaa [amp] yyy", S("aaa & yyy").sub(/&(?<foo>.*?);/, S('[\k<foo>]'))) + end + end + def test_sub! a = S("hello") b = a.dup @@ -2301,6 +2496,8 @@ CODE assert_not_predicate(str, :ascii_only?) assert_not_predicate(star, :ascii_only?) assert_not_predicate(result, :ascii_only?, bug13950) + + assert_equal(S("XYC"), S("ABC").tr("A-AB", "XY")) end def test_tr! @@ -2325,6 +2522,8 @@ CODE a = S("abc".force_encoding(Encoding::US_ASCII)) assert_nil(a.tr!(S("z"), S("\u0101")), '[ruby-core:22326]') assert_equal(Encoding::US_ASCII, a.encoding, '[ruby-core:22326]') + + assert_equal(S("XYC"), S("ABC").tr!("A-AB", "XY")) end def test_tr_s @@ -2332,6 +2531,8 @@ CODE assert_equal(S("h*o"), S("hello").tr_s(S("el"), S("*"))) assert_equal("a".hash, S("\u0101\u0101").tr_s("\u0101", "a").hash) assert_equal(true, S("\u3041\u3041").tr("\u3041", "a").ascii_only?) + + assert_equal(S("XYC"), S("ABC").tr_s("A-AB", "XY")) end def test_tr_s! @@ -2344,6 +2545,8 @@ CODE a = S("hello") assert_equal(S("h*o"), a.tr_s!(S("el"), S("*"))) assert_equal(S("h*o"), a) + + assert_equal(S("XYC"), S("ABC").tr_s!("A-AB", "XY")) end def test_unpack @@ -2396,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 @@ -2714,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 @@ -2772,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? @@ -2805,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 @@ -2880,14 +3089,15 @@ CODE s5 = S("\u0000\u3042") assert_equal("\u3042", s5.lstrip!) assert_equal("\u3042", s5) - end - def test_delete_prefix + def test_delete_prefix_type_error assert_raise(TypeError) { S('hello').delete_prefix(nil) } assert_raise(TypeError) { S('hello').delete_prefix(1) } assert_raise(TypeError) { S('hello').delete_prefix(/hel/) } + end + def test_delete_prefix s = S("hello") assert_equal("lo", s.delete_prefix('hel')) assert_equal("hello", s) @@ -2907,8 +3117,9 @@ CODE s = S("hello") assert_equal("hello", s.delete_prefix("\u{3053 3093}")) assert_equal("hello", s) + end - # skip if argument is a broken string + def test_delete_prefix_broken_encoding s = S("\xe3\x81\x82") assert_equal("\xe3\x81\x82", s.delete_prefix("\xe3")) assert_equal("\xe3\x81\x82", s) @@ -2917,23 +3128,31 @@ CODE assert_equal("\x95\x5c".force_encoding("Shift_JIS"), s.delete_prefix("\x95")) assert_equal("\x95\x5c".force_encoding("Shift_JIS"), s) - # clear coderange + assert_equal("\xFE", S("\xFF\xFE").delete_prefix("\xFF")) + assert_equal("\xBE", S("hello\xBE").delete_prefix("hello")) + assert_equal("\xBE", S("\xFFhello\xBE").delete_prefix("\xFFhello")) + end + + def test_delete_prefix_clear_coderange s = S("\u{3053 3093}hello") assert_not_predicate(s, :ascii_only?) assert_predicate(s.delete_prefix("\u{3053 3093}"), :ascii_only?) + end - # argument should be converted to String + def test_delete_prefix_argument_conversion klass = Class.new { def to_str; 'a'; end } s = S("abba") assert_equal("bba", s.delete_prefix(klass.new)) assert_equal("abba", s) end - def test_delete_prefix_bang + def test_delete_prefix_bang_type_error assert_raise(TypeError) { S('hello').delete_prefix!(nil) } assert_raise(TypeError) { S('hello').delete_prefix!(1) } assert_raise(TypeError) { S('hello').delete_prefix!(/hel/) } + end + def test_delete_prefix_bang s = S("hello") assert_equal("lo", s.delete_prefix!('hel')) assert_equal("lo", s) @@ -2953,23 +3172,32 @@ CODE s = S("hello") assert_equal(nil, s.delete_prefix!("\u{3053 3093}")) assert_equal("hello", s) + end - # skip if argument is a broken string + def test_delete_prefix_bang_broken_encoding s = S("\xe3\x81\x82") assert_equal(nil, s.delete_prefix!("\xe3")) assert_equal("\xe3\x81\x82", s) - # clear coderange + s = S("\xFF\xFE") + assert_equal("\xFE", s.delete_prefix!("\xFF")) + assert_equal("\xFE", s) + end + + def test_delete_prefix_bang_clear_coderange s = S("\u{3053 3093}hello") assert_not_predicate(s, :ascii_only?) assert_predicate(s.delete_prefix!("\u{3053 3093}"), :ascii_only?) + end - # argument should be converted to String + def test_delete_prefix_bang_argument_conversion klass = Class.new { def to_str; 'a'; end } s = S("abba") assert_equal("bba", s.delete_prefix!(klass.new)) assert_equal("bba", s) + end + def test_delete_prefix_bang_frozen_error s = S("ax").freeze assert_raise_with_message(FrozenError, /frozen/) {s.delete_prefix!("a")} @@ -2982,11 +3210,13 @@ CODE assert_raise_with_message(FrozenError, /frozen/) {s.delete_prefix!(o)} end - def test_delete_suffix + def test_delete_suffix_type_error assert_raise(TypeError) { S('hello').delete_suffix(nil) } assert_raise(TypeError) { S('hello').delete_suffix(1) } assert_raise(TypeError) { S('hello').delete_suffix(/hel/) } + end + def test_delete_suffix s = S("hello") assert_equal("hel", s.delete_suffix('lo')) assert_equal("hello", s) @@ -3006,23 +3236,28 @@ CODE s = S("hello") assert_equal("hello", s.delete_suffix("\u{3061 306f}")) assert_equal("hello", s) + end - # skip if argument is a broken string + def test_delete_suffix_broken_encoding s = S("\xe3\x81\x82") assert_equal("\xe3\x81\x82", s.delete_suffix("\x82")) assert_equal("\xe3\x81\x82", s) + end - # clear coderange + def test_delete_suffix_clear_coderange s = S("hello\u{3053 3093}") assert_not_predicate(s, :ascii_only?) assert_predicate(s.delete_suffix("\u{3053 3093}"), :ascii_only?) + end - # argument should be converted to String + def test_delete_suffix_argument_conversion klass = Class.new { def to_str; 'a'; end } s = S("abba") assert_equal("abb", s.delete_suffix(klass.new)) assert_equal("abba", s) + end + def test_delete_suffix_newline # chomp removes any of "\n", "\r\n", "\r" when "\n" is specified, # but delete_suffix does not s = "foo\n" @@ -3033,11 +3268,13 @@ CODE assert_equal("foo\r", s.delete_suffix("\n")) end - def test_delete_suffix_bang + def test_delete_suffix_bang_type_error assert_raise(TypeError) { S('hello').delete_suffix!(nil) } assert_raise(TypeError) { S('hello').delete_suffix!(1) } assert_raise(TypeError) { S('hello').delete_suffix!(/hel/) } + end + def test_delete_suffix_bang_frozen_error s = S("hello").freeze assert_raise_with_message(FrozenError, /frozen/) {s.delete_suffix!('lo')} @@ -3048,7 +3285,9 @@ CODE "x" end assert_raise_with_message(FrozenError, /frozen/) {s.delete_suffix!(o)} + end + def test_delete_suffix_bang s = S("hello") assert_equal("hel", s.delete_suffix!('lo')) assert_equal("hel", s) @@ -3068,8 +3307,9 @@ CODE s = S("hello") assert_equal(nil, s.delete_suffix!("\u{3061 306f}")) assert_equal("hello", s) + end - # skip if argument is a broken string + def test_delete_suffix_bang_broken_encoding s = S("\xe3\x81\x82") assert_equal(nil, s.delete_suffix!("\x82")) assert_equal("\xe3\x81\x82", s) @@ -3077,18 +3317,22 @@ CODE s = S("\x95\x5c").force_encoding("Shift_JIS") assert_equal(nil, s.delete_suffix!("\x5c")) assert_equal("\x95\x5c".force_encoding("Shift_JIS"), s) + end - # clear coderange + def test_delete_suffix_bang_clear_coderange s = S("hello\u{3053 3093}") assert_not_predicate(s, :ascii_only?) assert_predicate(s.delete_suffix!("\u{3053 3093}"), :ascii_only?) + end - # argument should be converted to String + def test_delete_suffix_bang_argument_conversion klass = Class.new { def to_str; 'a'; end } s = S("abba") assert_equal("abb", s.delete_suffix!(klass.new)) assert_equal("abb", s) + end + def test_delete_suffix_bang_newline # chomp removes any of "\n", "\r\n", "\r" when "\n" is specified, # but delete_suffix does not s = "foo\n" @@ -3144,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 @@ -3212,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) @@ -3279,7 +3522,11 @@ CODE assert_same(str, +str) assert_not_same(str, -str) - str = "bar".freeze + require 'objspace' + + str = "test_uplus_minus_str".freeze + assert_includes ObjectSpace.dump(str), '"fstring":true' + assert_predicate(str, :frozen?) assert_not_predicate(+str, :frozen?) assert_predicate(-str, :frozen?) @@ -3287,8 +3534,14 @@ CODE assert_not_same(str, +str) assert_same(str, -str) - bar = %w(b a r).join('') - assert_same(str, -bar, "uminus deduplicates [Feature #13077]") + bar = -%w(test uplus minus str).join('_') + assert_same(str, bar, "uminus deduplicates [Feature #13077] str: #{ObjectSpace.dump(str)} bar: #{ObjectSpace.dump(bar)}") + end + + def test_uminus_dedup_in_place + dynamic = "this string is unique and frozen #{rand}".freeze + assert_same dynamic, -dynamic + assert_same dynamic, -dynamic.dup end def test_uminus_frozen @@ -3325,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) @@ -3346,111 +3610,114 @@ CODE end def test_byteindex - assert_equal(0, S("hello").byteindex(?h)) - assert_equal(1, S("hello").byteindex(S("ell"))) - assert_equal(2, S("hello").byteindex(/ll./)) + assert_byteindex(0, S("hello"), ?h) + assert_byteindex(1, S("hello"), S("ell")) + assert_byteindex(2, S("hello"), /ll./) - assert_equal(3, S("hello").byteindex(?l, 3)) - assert_equal(3, S("hello").byteindex(S("l"), 3)) - assert_equal(3, S("hello").byteindex(/l./, 3)) + assert_byteindex(3, S("hello"), ?l, 3) + assert_byteindex(3, S("hello"), S("l"), 3) + assert_byteindex(3, S("hello"), /l./, 3) - assert_nil(S("hello").byteindex(?z, 3)) - assert_nil(S("hello").byteindex(S("z"), 3)) - assert_nil(S("hello").byteindex(/z./, 3)) + assert_byteindex(nil, S("hello"), ?z, 3) + assert_byteindex(nil, S("hello"), S("z"), 3) + assert_byteindex(nil, S("hello"), /z./, 3) - assert_nil(S("hello").byteindex(?z)) - assert_nil(S("hello").byteindex(S("z"))) - assert_nil(S("hello").byteindex(/z./)) + assert_byteindex(nil, S("hello"), ?z) + assert_byteindex(nil, S("hello"), S("z")) + assert_byteindex(nil, S("hello"), /z./) - assert_equal(0, S("").byteindex(S(""))) - assert_equal(0, S("").byteindex(//)) - assert_nil(S("").byteindex(S("hello"))) - assert_nil(S("").byteindex(/hello/)) - assert_equal(0, S("hello").byteindex(S(""))) - assert_equal(0, S("hello").byteindex(//)) + assert_byteindex(0, S(""), S("")) + assert_byteindex(0, S(""), //) + assert_byteindex(nil, S(""), S("hello")) + assert_byteindex(nil, S(""), /hello/) + assert_byteindex(0, S("hello"), S("")) + assert_byteindex(0, S("hello"), //) s = S("long") * 1000 << "x" - assert_nil(s.byteindex(S("y"))) - assert_equal(4 * 1000, s.byteindex(S("x"))) + assert_byteindex(nil, s, S("y")) + assert_byteindex(4 * 1000, s, S("x")) s << "yx" - assert_equal(4 * 1000, s.byteindex(S("x"))) - assert_equal(4 * 1000, s.byteindex(S("xyx"))) + assert_byteindex(4 * 1000, s, S("x")) + assert_byteindex(4 * 1000, s, S("xyx")) o = Object.new def o.to_str; "bar"; end - assert_equal(3, S("foobarbarbaz").byteindex(o)) + assert_byteindex(3, S("foobarbarbaz"), o) assert_raise(TypeError) { S("foo").byteindex(Object.new) } - assert_nil(S("foo").byteindex(//, -100)) - assert_nil($~) + assert_byteindex(nil, S("foo"), //, -100) + assert_byteindex(nil, S("foo"), //, -4) - assert_equal(2, S("abcdbce").byteindex(/b\Kc/)) + assert_byteindex(2, S("abcdbce"), /b\Kc/) - assert_equal(0, S("ã“ã‚“ã«ã¡ã¯").byteindex(?ã“)) - assert_equal(3, S("ã“ã‚“ã«ã¡ã¯").byteindex(S("ã‚“ã«ã¡"))) - assert_equal(6, S("ã“ã‚“ã«ã¡ã¯").byteindex(/ã«ã¡./)) + assert_byteindex(0, S("ã“ã‚“ã«ã¡ã¯"), ?ã“) + assert_byteindex(3, S("ã“ã‚“ã«ã¡ã¯"), S("ã‚“ã«ã¡")) + assert_byteindex(6, S("ã“ã‚“ã«ã¡ã¯"), /ã«ã¡./) - assert_equal(0, S("ã«ã‚“ã«ã¡ã¯").byteindex(?ã«, 0)) + assert_byteindex(0, S("ã«ã‚“ã«ã¡ã¯"), ?ã«, 0) assert_raise(IndexError) { S("ã«ã‚“ã«ã¡ã¯").byteindex(?ã«, 1) } assert_raise(IndexError) { S("ã«ã‚“ã«ã¡ã¯").byteindex(?ã«, 5) } - assert_equal(6, S("ã«ã‚“ã«ã¡ã¯").byteindex(?ã«, 6)) - assert_equal(6, S("ã«ã‚“ã«ã¡ã¯").byteindex(S("ã«"), 6)) - assert_equal(6, S("ã«ã‚“ã«ã¡ã¯").byteindex(/ã«./, 6)) + assert_byteindex(6, S("ã«ã‚“ã«ã¡ã¯"), ?ã«, 6) + assert_byteindex(6, S("ã«ã‚“ã«ã¡ã¯"), S("ã«"), 6) + assert_byteindex(6, S("ã«ã‚“ã«ã¡ã¯"), /ã«./, 6) assert_raise(IndexError) { S("ã«ã‚“ã«ã¡ã¯").byteindex(?ã«, 7) } + + s = S("foobarbarbaz") + assert !1000.times.any? {s.byteindex("", 100_000_000)} end def test_byterindex - assert_equal(3, S("hello").byterindex(?l)) - assert_equal(6, S("ell, hello").byterindex(S("ell"))) - assert_equal(7, S("ell, hello").byterindex(/ll./)) + assert_byterindex(3, S("hello"), ?l) + assert_byterindex(6, S("ell, hello"), S("ell")) + assert_byterindex(7, S("ell, hello"), /ll./) - assert_equal(3, S("hello,lo").byterindex(?l, 3)) - assert_equal(3, S("hello,lo").byterindex(S("l"), 3)) - assert_equal(3, S("hello,lo").byterindex(/l./, 3)) + assert_byterindex(3, S("hello,lo"), ?l, 3) + assert_byterindex(3, S("hello,lo"), S("l"), 3) + assert_byterindex(3, S("hello,lo"), /l./, 3) - assert_nil(S("hello").byterindex(?z, 3)) - assert_nil(S("hello").byterindex(S("z"), 3)) - assert_nil(S("hello").byterindex(/z./, 3)) + assert_byterindex(nil, S("hello"), ?z, 3) + assert_byterindex(nil, S("hello"), S("z"), 3) + assert_byterindex(nil, S("hello"), /z./, 3) - assert_nil(S("hello").byterindex(?z)) - assert_nil(S("hello").byterindex(S("z"))) - assert_nil(S("hello").byterindex(/z./)) + assert_byterindex(nil, S("hello"), ?z) + assert_byterindex(nil, S("hello"), S("z")) + assert_byterindex(nil, S("hello"), /z./) - assert_equal(5, S("hello").byterindex(S(""))) - assert_equal(5, S("hello").byterindex(S(""), 5)) - assert_equal(4, S("hello").byterindex(S(""), 4)) - assert_equal(0, S("hello").byterindex(S(""), 0)) + assert_byterindex(5, S("hello"), S("")) + assert_byterindex(5, S("hello"), S(""), 5) + assert_byterindex(4, S("hello"), S(""), 4) + assert_byterindex(0, S("hello"), S(""), 0) o = Object.new def o.to_str; "bar"; end - assert_equal(6, S("foobarbarbaz").byterindex(o)) + assert_byterindex(6, S("foobarbarbaz"), o) assert_raise(TypeError) { S("foo").byterindex(Object.new) } - assert_nil(S("foo").byterindex(//, -100)) - assert_nil($~) + assert_byterindex(nil, S("foo"), //, -100) - assert_equal(3, S("foo").byterindex(//)) - assert_equal([3, 3], $~.offset(0)) + m = assert_byterindex(3, S("foo"), //) + assert_equal([3, 3], m.offset(0)) + assert_byterindex(3, S("foo"), //, 4) - assert_equal(5, S("abcdbce").byterindex(/b\Kc/)) + assert_byterindex(5, S("abcdbce"), /b\Kc/) - assert_equal(6, S("ã“ã‚“ã«ã¡ã¯").byterindex(?ã«)) - assert_equal(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"))) - assert_equal(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(/ã«ã¡./)) + assert_byterindex(6, S("ã“ã‚“ã«ã¡ã¯"), ?ã«) + assert_byterindex(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯")) + assert_byterindex(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), /ã«ã¡./) assert_raise(IndexError) { S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), 19) } assert_raise(IndexError) { S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), -2) } - assert_equal(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), 18)) - assert_equal(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), -3)) + assert_byterindex(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), 18) + assert_byterindex(18, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), -3) assert_raise(IndexError) { S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), 17) } assert_raise(IndexError) { S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), -4) } assert_raise(IndexError) { S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), 1) } - assert_equal(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯").byterindex(S("ã«ã¡ã¯"), 0)) + assert_byterindex(0, S("ã«ã¡ã¯ã€ã“ã‚“ã«ã¡ã¯"), S("ã«ã¡ã¯"), 0) - assert_equal(0, S("ã“ã‚“ã«ã¡ã¯").byterindex(S("ã“ã‚“ã«ã¡ã¯"))) - assert_nil(S("ã“ã‚“ã«ã¡").byterindex(S("ã“ã‚“ã«ã¡ã¯"))) - assert_nil(S("ã“").byterindex(S("ã“ã‚“ã«ã¡ã¯"))) - assert_nil(S("").byterindex(S("ã“ã‚“ã«ã¡ã¯"))) + assert_byterindex(0, S("ã“ã‚“ã«ã¡ã¯"), S("ã“ã‚“ã«ã¡ã¯")) + assert_byterindex(nil, S("ã“ã‚“ã«ã¡"), S("ã“ã‚“ã«ã¡ã¯")) + assert_byterindex(nil, S("ã“"), S("ã“ã‚“ã«ã¡ã¯")) + assert_byterindex(nil, S(""), S("ã“ã‚“ã«ã¡ã¯")) end def test_bytesplice @@ -3530,6 +3797,194 @@ CODE assert_bytesplice_raise(ArgumentError, S("hello"), 0..-1, "bye", 0, 3) end + def test_append_bytes_into_binary + buf = S("".b) + assert_equal Encoding::BINARY, buf.encoding + + buf.append_as_bytes(S("hello")) + assert_equal "hello".b, buf + assert_equal Encoding::BINARY, buf.encoding + + buf.append_as_bytes(S("ã“ã‚“ã«ã¡ã¯")) + assert_equal S("helloã“ã‚“ã«ã¡ã¯".b), buf + assert_equal Encoding::BINARY, buf.encoding + end + + def test_append_bytes_into_utf8 + buf = S("") + assert_equal Encoding::UTF_8, buf.encoding + + buf.append_as_bytes(S("hello")) + assert_equal S("hello"), buf + assert_equal Encoding::UTF_8, buf.encoding + assert_predicate buf, :ascii_only? + assert_predicate buf, :valid_encoding? + + buf.append_as_bytes(S("ã“ã‚“ã«ã¡ã¯")) + assert_equal S("helloã“ã‚“ã«ã¡ã¯"), buf + assert_equal Encoding::UTF_8, buf.encoding + refute_predicate buf, :ascii_only? + assert_predicate buf, :valid_encoding? + + buf.append_as_bytes(S("\xE2\x82".b)) + assert_equal S("helloã“ã‚“ã«ã¡ã¯\xE2\x82"), buf + assert_equal Encoding::UTF_8, buf.encoding + refute_predicate buf, :valid_encoding? + + buf.append_as_bytes(S("\xAC".b)) + assert_equal S("helloã“ã‚“ã«ã¡ã¯â‚¬"), buf + assert_equal Encoding::UTF_8, buf.encoding + assert_predicate buf, :valid_encoding? + end + + def test_append_bytes_into_utf32 + buf = S("abc".encode(Encoding::UTF_32LE)) + assert_equal Encoding::UTF_32LE, buf.encoding + + buf.append_as_bytes("def") + assert_equal Encoding::UTF_32LE, buf.encoding + refute_predicate buf, :valid_encoding? + end + + def test_chilled_string + chilled_string = eval('"chilled"') + + assert_not_predicate chilled_string, :frozen? + + assert_not_predicate chilled_string.dup, :frozen? + assert_not_predicate chilled_string.clone, :frozen? + + # @+ treat the original string as frozen + assert_not_predicate(+chilled_string, :frozen?) + assert_not_same chilled_string, +chilled_string + + # @- treat the original string as mutable + assert_predicate(-chilled_string, :frozen?) + assert_not_same chilled_string, -chilled_string + end + + def test_chilled_string_setivar + deprecated = Warning[:deprecated] + Warning[:deprecated] = false + + String.class_eval <<~RUBY, __FILE__, __LINE__ + 1 + def setivar! + @ivar = 42 + @ivar + end + RUBY + chilled_string = eval('"chilled"') + begin + assert_equal 42, chilled_string.setivar! + ensure + String.undef_method(:setivar!) + end + ensure + Warning[:deprecated] = deprecated + end + + def test_chilled_string_substring + deprecated = Warning[:deprecated] + Warning[:deprecated] = false + chilled_string = eval('"a chilled string."') + substring = chilled_string[0..-1] + assert_equal("a chilled string.", substring) + chilled_string[0..-1] = "This string is defrosted." + assert_equal("a chilled string.", substring) + ensure + 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) @@ -3540,6 +3995,46 @@ CODE def assert_bytesplice_raise(e, s, *args) assert_raise(e) { s.send(:bytesplice, *args) } end + + def assert_index_like(method, expected, string, match, *rest) + message = "#{method} with string does not affect $~" + /.*/ =~ message + md_before = $~ + assert_equal(expected, string.__send__(method, match, *rest)) + md_after = $~ + case match + when Regexp + if expected + assert_not_nil(md_after) + assert_not_same(md_before, md_after) + else + assert_nil(md_after) + end + else + assert_same(md_before, md_after) + end + md_after + end + + def assert_index(expected, string, match, *rest) + assert_index_like(:index, expected, string, match, *rest) + end + + def assert_rindex(expected, string, match, *rest) + assert_index_like(:rindex, expected, string, match, *rest) + end + + def assert_byteindex(expected, string, match, *rest) + assert_index_like(:byteindex, expected, string, match, *rest) + end + + 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_string_memory.rb b/test/ruby/test_string_memory.rb new file mode 100644 index 0000000000..a93a3bd54a --- /dev/null +++ b/test/ruby/test_string_memory.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: false +require 'test/unit' +require 'objspace' + +class TestStringMemory < Test::Unit::TestCase + def capture_allocations(klass) + allocations = [] + + EnvUtil.without_gc do + GC.start + generation = GC.count + + ObjectSpace.trace_object_allocations do + yield + + ObjectSpace.each_object(klass) do |instance| + allocations << instance if ObjectSpace.allocation_generation(instance) == generation + end + end + + return allocations.map do |instance| + [ + ObjectSpace.allocation_sourcefile(instance), + ObjectSpace.allocation_sourceline(instance), + instance.class, + instance, + ] + end.select do |path,| + # drop strings not created in this file + # (the parallel testing framework may create strings in a separate thread) + path == __FILE__ + end + end + end + + def test_byteslice_prefix + string = ("a" * 100_000).freeze + + allocations = capture_allocations(String) do + string.byteslice(0, 50_000) + end + + assert_equal 1, allocations.size, "One object allocation is expected, but allocated: #{ allocations.inspect }" + end + + def test_byteslice_postfix + string = ("a" * 100_000).freeze + + allocations = capture_allocations(String) do + string.byteslice(50_000, 100_000) + end + + assert_equal 1, allocations.size, "One object allocation is expected, but allocated: #{ allocations.inspect }" + end + + def test_byteslice_postfix_twice + string = ("a" * 100_000).freeze + + allocations = capture_allocations(String) do + string.byteslice(50_000, 100_000).byteslice(25_000, 50_000) + end + + assert_equal 2, allocations.size, "Two object allocations are expected, but allocated: #{ allocations.inspect }" + end +end diff --git a/test/ruby/test_struct.rb b/test/ruby/test_struct.rb index 78a81c5200..01e5cc68f6 100644 --- a/test/ruby/test_struct.rb +++ b/test/ruby/test_struct.rb @@ -41,6 +41,14 @@ module TestStruct end end + def test_larger_than_largest_pool + count = (GC::INTERNAL_CONSTANTS[:RVARGC_MAX_ALLOCATE_SIZE] / RbConfig::SIZEOF["void*"]) + 1 + list = Array(0..count) + klass = @Struct.new(*list.map { |i| :"a_#{i}"}) + struct = klass.new(*list) + assert_equal 0, struct.a_0 + end + def test_small_structs names = [:a, :b, :c, :d] 1.upto(4) {|n| @@ -526,6 +534,28 @@ module TestStruct assert_equal [[:req, :_]], klass.instance_method(:c=).parameters 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 + Struct.new("A") + Struct.send(:remove_const, :A) + end + + 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 6a575b88c5..25bad2242a 100644 --- a/test/ruby/test_super.rb +++ b/test/ruby/test_super.rb @@ -8,6 +8,7 @@ class TestSuper < Test::Unit::TestCase def array(*a) a end def optional(a = 0) a end def keyword(**a) a end + def forward(*a) a end end class Single1 < Base def single(*) super end @@ -63,6 +64,16 @@ class TestSuper < Test::Unit::TestCase [x, y] end end + class Forward < Base + def forward(...) + w = super() + x = super + y = super(...) + a = 1 + z = super(a, ...) + [w, x, y, z] + end + end def test_single1 assert_equal(1, Single1.new.single(1)) @@ -133,6 +144,11 @@ class TestSuper < Test::Unit::TestCase def test_keyword2 assert_equal([{foo: "changed1"}, {foo: "changed2"}], Keyword2.new.keyword) end + def test_forwardable(...) + assert_equal([[],[],[],[1]], Forward.new.forward()) + assert_equal([[],[1,2],[1,2],[1,1,2]], Forward.new.forward(1,2)) + assert_equal([[],[:test],[:test],[1,:test]], Forward.new.forward(:test, ...)) + end class A def tt(aa) @@ -558,6 +574,18 @@ class TestSuper < Test::Unit::TestCase end end + def test_zsuper_kw_splat_not_mutable + extend(Module.new{def a(**k) k[:a] = 1 end}) + extend(Module.new do + def a(**k) + before = k.dup + super + [before, k] + end + end) + assert_equal(*a) + end + def test_from_eval bug10263 = '[ruby-core:65122] [Bug #10263a]' a = Class.new do @@ -605,6 +633,40 @@ class TestSuper < Test::Unit::TestCase } end + def test_super_with_included_prepended_module_method_caching_bug_20716 + a = Module.new do + def test(*args) + super + end + end + + b = Module.new do + def test(a) + a + end + end + + c = Class.new + + b.prepend(a) + c.include(b) + + assert_equal(1, c.new.test(1)) + + b.class_eval do + begin + verbose_bak, $VERBOSE = $VERBOSE, nil + def test + :test + end + ensure + $VERBOSE = verbose_bak + end + end + + assert_equal(:test, c.new.test) + end + class TestFor_super_with_modified_rest_parameter_base def foo *args args @@ -697,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_symbol.rb b/test/ruby/test_symbol.rb index 1d2a18d734..c50febf5d1 100644 --- a/test/ruby/test_symbol.rb +++ b/test/ruby/test_symbol.rb @@ -90,12 +90,15 @@ class TestSymbol < Test::Unit::TestCase end def test_inspect_dollar + verbose_bak, $VERBOSE = $VERBOSE, nil # 4) :$- always treats next character literally: assert_raise(SyntaxError) {eval ':$-'} assert_raise(SyntaxError) {eval ":$-\n"} assert_raise(SyntaxError) {eval ":$- "} assert_raise(SyntaxError) {eval ":$-#"} assert_raise(SyntaxError) {eval ':$-('} + ensure + $VERBOSE = verbose_bak end def test_inspect_number @@ -118,6 +121,14 @@ class TestSymbol < Test::Unit::TestCase end end + def test_inspect_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + + EnvUtil.under_gc_compact_stress do + assert_inspect_evaled(':testing') + end + end + def test_name assert_equal("foo", :foo.name) assert_same(:foo.name, :foo.name) diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index f9416f8fcb..b355128a73 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -13,8 +13,7 @@ class TestSyntax < Test::Unit::TestCase def assert_syntax_files(test) srcdir = File.expand_path("../../..", __FILE__) srcdir = File.join(srcdir, test) - assert_separately(%W[--disable-gem - #{srcdir}], - __FILE__, __LINE__, <<-'eom', timeout: Float::INFINITY) + assert_separately(%W[- #{srcdir}], __FILE__, __LINE__, <<-'eom', timeout: Float::INFINITY) dir = ARGV.shift for script in Dir["#{dir}/**/*.rb"].sort assert_valid_syntax(IO::read(script), script) @@ -77,97 +76,120 @@ class TestSyntax < Test::Unit::TestCase def test_anonymous_block_forwarding assert_syntax_error("def b; c(&); end", /no anonymous block parameter/) + assert_syntax_error("def b(&) ->(&) {c(&)} end", /anonymous block parameter is also used/) + assert_valid_syntax("def b(&) ->() {c(&)} end") assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") begin; - def b(&); c(&) end - def c(&); yield 1 end - a = nil - b{|c| a = c} - assert_equal(1, a) - - def inner - yield - end + def b(&); c(&) end + def c(&); yield 1 end + a = nil + b{|c| a = c} + assert_equal(1, a) + + def inner + yield + end - def block_only(&) - inner(&) - end - assert_equal(1, block_only{1}) + def block_only(&) + inner(&) + end + assert_equal(1, block_only{1}) - def pos(arg1, &) - inner(&) - end - assert_equal(2, pos(nil){2}) + def pos(arg1, &) + inner(&) + end + assert_equal(2, pos(nil){2}) - def pos_kwrest(arg1, **kw, &) - inner(&) - end - assert_equal(3, pos_kwrest(nil){3}) + def pos_kwrest(arg1, **kw, &) + inner(&) + end + assert_equal(3, pos_kwrest(nil){3}) - def no_kw(arg1, **nil, &) - inner(&) - end - assert_equal(4, no_kw(nil){4}) + def no_kw(arg1, **nil, &) + inner(&) + end + assert_equal(4, no_kw(nil){4}) - def rest_kw(*a, kwarg: 1, &) - inner(&) - end - assert_equal(5, rest_kw{5}) + def rest_kw(*a, kwarg: 1, &) + inner(&) + end + assert_equal(5, rest_kw{5}) - def kw(kwarg:1, &) - inner(&) - end - assert_equal(6, kw{6}) + def kw(kwarg:1, &) + inner(&) + end + assert_equal(6, kw{6}) - def pos_kw_kwrest(arg1, kwarg:1, **kw, &) - inner(&) - end - assert_equal(7, pos_kw_kwrest(nil){7}) + def pos_kw_kwrest(arg1, kwarg:1, **kw, &) + inner(&) + end + assert_equal(7, pos_kw_kwrest(nil){7}) - def pos_rkw(arg1, kwarg1:, &) - inner(&) - end - assert_equal(8, pos_rkw(nil, kwarg1: nil){8}) + def pos_rkw(arg1, kwarg1:, &) + inner(&) + end + assert_equal(8, pos_rkw(nil, kwarg1: nil){8}) - def all(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, &) - inner(&) - end - assert_equal(9, all(nil, nil, nil, nil, okw1: nil, okw2: nil){9}) + def all(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, &) + inner(&) + end + assert_equal(9, all(nil, nil, nil, nil, okw1: nil, okw2: nil){9}) - def all_kwrest(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, **kw, &) - inner(&) - end - assert_equal(10, all_kwrest(nil, nil, nil, nil, okw1: nil, okw2: nil){10}) + def all_kwrest(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, **kw, &) + inner(&) + end + assert_equal(10, all_kwrest(nil, nil, nil, nil, okw1: nil, okw2: nil){10}) + + def evaled(&) + eval("inner(&)") + end + assert_equal(1, evaled{1}) end; end def test_anonymous_rest_forwarding assert_syntax_error("def b; c(*); end", /no anonymous rest parameter/) assert_syntax_error("def b; c(1, *); end", /no anonymous rest parameter/) + assert_syntax_error("def b(*) ->(*) {c(*)} end", /anonymous rest parameter is also used/) + assert_syntax_error("def b(a, *) ->(*) {c(1, *)} end", /anonymous rest parameter is also used/) + assert_syntax_error("def b(*) ->(a, *) {c(*)} end", /anonymous rest parameter is also used/) + assert_valid_syntax("def b(*) ->() {c(*)} end") + assert_valid_syntax("def b(a, *) ->() {c(1, *)} end") + assert_valid_syntax("def b(*) ->(a) {c(*)} end") assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") begin; - def b(*); c(*) end - def c(*a); a end - def d(*); b(*, *) end - assert_equal([1, 2], b(1, 2)) - assert_equal([1, 2, 1, 2], d(1, 2)) + def b(*); c(*) end + def c(*a); a end + def d(*); b(*, *) end + def e(*); eval("b(*)") end + assert_equal([1, 2], b(1, 2)) + assert_equal([1, 2, 1, 2], d(1, 2)) + assert_equal([1, 2], e(1, 2)) end; end def test_anonymous_keyword_rest_forwarding assert_syntax_error("def b; c(**); end", /no anonymous keyword rest parameter/) assert_syntax_error("def b; c(k: 1, **); end", /no anonymous keyword rest parameter/) + assert_syntax_error("def b(**) ->(**) {c(**)} end", /anonymous keyword rest parameter is also used/) + assert_syntax_error("def b(k:, **) ->(**) {c(k: 1, **)} end", /anonymous keyword rest parameter is also used/) + assert_syntax_error("def b(**) ->(k:, **) {c(**)} end", /anonymous keyword rest parameter is also used/) + assert_valid_syntax("def b(**) ->() {c(**)} end") + assert_valid_syntax("def b(k:, **) ->() {c(k: 1, **)} end") + assert_valid_syntax("def b(**) ->(k:) {c(**)} end") assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") begin; - def b(**); c(**) end - def c(**kw); kw end - def d(**); b(k: 1, **) end - def e(**); b(**, k: 1) end - def f(a: nil, **); b(**) end - assert_equal({a: 1, k: 3}, b(a: 1, k: 3)) - assert_equal({a: 1, k: 3}, d(a: 1, k: 3)) - assert_equal({a: 1, k: 1}, e(a: 1, k: 3)) - assert_equal({k: 3}, f(a: 1, k: 3)) + def b(**); c(**) end + def c(**kw); kw end + def d(**); b(k: 1, **) end + def e(**); b(**, k: 1) end + def f(a: nil, **); b(**) end + def g(**); eval("b(**)") end + assert_equal({a: 1, k: 3}, b(a: 1, k: 3)) + assert_equal({a: 1, k: 3}, d(a: 1, k: 3)) + assert_equal({a: 1, k: 1}, e(a: 1, k: 3)) + assert_equal({k: 3}, f(a: 1, k: 3)) + assert_equal({a: 1, k: 3}, g(a: 1, k: 3)) end; end @@ -177,6 +199,7 @@ class TestSyntax < Test::Unit::TestCase assert_syntax_error("def f(...); g(0, *); end", /no anonymous rest parameter/) assert_syntax_error("def f(...); g(**); end", /no anonymous keyword rest parameter/) assert_syntax_error("def f(...); g(x: 1, **); end", /no anonymous keyword rest parameter/) + assert_syntax_error("def f(...); g(&); end", /no anonymous block parameter/) end def test_newline_in_block_parameters @@ -194,7 +217,7 @@ class TestSyntax < Test::Unit::TestCase blocks = [['do end', 'do'], ['{}', 'brace']], *| [%w'. dot', %w':: colon'].product(methods, blocks) do |(c, n1), (m, n2), (b, n3)| - m = m.tr_s('()', ' ').strip if n2 == 'do' + m = m.tr_s('()', ' ').strip if n3 == 'do' name = "test_#{n3}_block_after_blockcall_#{n1}_#{n2}_arg" code = "#{blockcall}#{c}#{m} #{b}" define_method(name) {assert_valid_syntax(code, bug6115)} @@ -220,6 +243,7 @@ class TestSyntax < Test::Unit::TestCase def test_array_kwsplat_hash kw = {} h = {a: 1} + a = [] assert_equal([], [**{}]) assert_equal([], [**kw]) assert_equal([h], [**h]) @@ -234,6 +258,20 @@ class TestSyntax < Test::Unit::TestCase assert_equal([1, kw], [1, kw]) assert_equal([1, h], [1, h]) + assert_equal([], [*a, **{}]) + assert_equal([], [*a, **kw]) + assert_equal([h], [*a, **h]) + assert_equal([{}], [*a, {}]) + assert_equal([kw], [*a, kw]) + assert_equal([h], [*a, h]) + + assert_equal([1], [1, *a, **{}]) + assert_equal([1], [1, *a, **kw]) + assert_equal([1, h], [1, *a, **h]) + assert_equal([1, {}], [1, *a, {}]) + assert_equal([1, kw], [1, *a, kw]) + assert_equal([1, h], [1, *a, h]) + assert_equal([], [**kw, **kw]) assert_equal([], [**kw, **{}, **kw]) assert_equal([1], [1, **kw, **{}, **kw]) @@ -301,7 +339,12 @@ class TestSyntax < Test::Unit::TestCase bug10315 = '[ruby-core:65368] [Bug #10315]' o = KW2.new - assert_equal([23, 2], o.kw(**{k1: 22}, **{k1: 23}), bug10315) + begin + verbose_bak, $VERBOSE = $VERBOSE, nil + assert_equal([23, 2], eval("o.kw(**{k1: 22}, **{k1: 23})"), bug10315) + ensure + $VERBOSE = verbose_bak + end h = {k3: 31} assert_raise(ArgumentError) {o.kw(**h)} @@ -336,6 +379,12 @@ class TestSyntax < Test::Unit::TestCase assert_warn(/duplicated/) {r = eval("a.f(**{k: a.add(1), j: a.add(2), k: a.add(3), k: a.add(4)})")} assert_equal(4, r) assert_equal([1, 2, 3, 4], a) + a.clear + r = nil + _z = {} + assert_warn(/duplicated/) {r = eval("a.f(k: a.add(1), **_z, k: a.add(2))")} + assert_equal(2, r) + assert_equal([1, 2], a) end def test_keyword_empty_splat @@ -357,11 +406,11 @@ class TestSyntax < Test::Unit::TestCase end def test_keyword_self_reference - message = /circular argument reference - var/ - assert_syntax_error("def foo(var: defined?(var)) var end", message) - assert_syntax_error("def foo(var: var) var end", message) - assert_syntax_error("def foo(var: bar(var)) var end", message) - assert_syntax_error("def foo(var: bar {var}) var end", message) + assert_valid_syntax("def foo(var: defined?(var)) var end") + assert_valid_syntax("def foo(var: var) var end") + assert_valid_syntax("def foo(var: bar(var)) var end") + assert_valid_syntax("def foo(var: bar {var}) var end") + assert_valid_syntax("def foo(var: (1 in ^var)); end") o = Object.new assert_warn("") do @@ -387,6 +436,9 @@ class TestSyntax < Test::Unit::TestCase assert_warn("") do o.instance_eval("proc {|var: 1| var}") end + + o = Object.new + assert_nil(o.instance_eval("def foo(bar: bar) = bar; foo")) end def test_keyword_invalid_name @@ -420,13 +472,13 @@ class TestSyntax < Test::Unit::TestCase end def test_optional_self_reference - message = /circular argument reference - var/ - assert_syntax_error("def foo(var = defined?(var)) var end", message) - assert_syntax_error("def foo(var = var) var end", message) - assert_syntax_error("def foo(var = bar(var)) var end", message) - assert_syntax_error("def foo(var = bar {var}) var end", message) - assert_syntax_error("def foo(var = (def bar;end; var)) var end", message) - assert_syntax_error("def foo(var = (def self.bar;end; var)) var end", message) + assert_valid_syntax("def foo(var = defined?(var)) var end") + assert_valid_syntax("def foo(var = var) var end") + assert_valid_syntax("def foo(var = bar(var)) var end") + assert_valid_syntax("def foo(var = bar {var}) var end") + assert_valid_syntax("def foo(var = (def bar;end; var)) var end") + assert_valid_syntax("def foo(var = (def self.bar;end; var)) var end") + assert_valid_syntax("def foo(var = (1 in ^var)); end") o = Object.new assert_warn("") do @@ -452,6 +504,9 @@ class TestSyntax < Test::Unit::TestCase assert_warn("") do o.instance_eval("proc {|var = 1| var}") end + + o = Object.new + assert_nil(o.instance_eval("def foo(bar: bar) = bar; foo")) end def test_warn_grouped_expression @@ -469,10 +524,6 @@ class TestSyntax < Test::Unit::TestCase end def test_warn_balanced - warning = <<WARN -test:1: warning: `%s' after local variable or literal is interpreted as binary operator -test:1: warning: even though it seems like %s -WARN [ [:**, "argument prefix"], [:*, "argument prefix"], @@ -486,7 +537,9 @@ WARN all_assertions do |a| ["puts 1 #{op}0", "puts :a #{op}0", "m = 1; puts m #{op}0"].each do |src| a.for(src) do - assert_warning(warning % [op, syn], src) do + warning = /'#{Regexp.escape(op)}' after local variable or literal is interpreted as binary operator.+?even though it seems like #{syn}/m + + assert_warning(warning, src) do assert_valid_syntax(src, "test", verbose: true) end end @@ -637,6 +690,8 @@ WARN assert_equal(42, obj.foo(42)) assert_equal(42, obj.foo(2, _: 0)) assert_equal(2, obj.foo(x: 2, _: 0)) + ensure + self.class.remove_method(:foo) end def test_duplicated_opt_kw @@ -676,8 +731,8 @@ WARN end def test_duplicated_when - w = 'warning: duplicated `when\' clause with line 3 is ignored' - assert_warning(/3: #{w}.+4: #{w}.+4: #{w}.+5: #{w}.+5: #{w}/m) { + w = ->(line) { "warning: 'when' clause on line #{line} duplicates 'when' clause on line 3 and is ignored" } + assert_warning(/#{w[3]}.+#{w[4]}.+#{w[4]}.+#{w[5]}.+#{w[5]}/m) { eval %q{ case 1 when 1, 1 @@ -686,7 +741,7 @@ WARN end } } - assert_warning(/#{w}/) {#/3: #{w}.+4: #{w}.+5: #{w}.+5: #{w}/m){ + assert_warning(/#{w[3]}.+#{w[4]}.+#{w[5]}.+#{w[5]}/m) { a = a = 1 eval %q{ case 1 @@ -696,10 +751,28 @@ WARN end } } + assert_warning(/#{w[3]}/) { + eval %q{ + case 1 + when __LINE__, __LINE__ + when 3, 3 + when 3, 3 + end + } + } + assert_warning(/#{w[3]}/) { + eval %q{ + case 1 + when __FILE__, __FILE__ + when "filename", "filename" + when "filename", "filename" + end + }, binding, "filename" + } end def test_duplicated_when_check_option - w = /duplicated `when\' clause with line 3 is ignored/ + w = /'when' clause on line 4 duplicates 'when' clause on line 3 and is ignored/ assert_in_out_err(%[-wc], "#{<<~"begin;"}\n#{<<~'end;'}", ["Syntax OK"], w) begin; case 1 @@ -832,6 +905,16 @@ e" assert_dedented_heredoc(expect, result) end + def test_dedented_heredoc_with_leading_blank_line + # the blank line has six leading spaces + result = " \n" \ + " b\n" + expect = " \n" \ + "b\n" + assert_dedented_heredoc(expect, result) + end + + def test_dedented_heredoc_with_blank_more_indented_line_escaped result = " a\n" \ "\\ \\ \\ \\ \\ \\ \n" \ @@ -939,7 +1022,7 @@ eom end def test_dedented_heredoc_concatenation - assert_equal("\n0\n1", eval("<<~0 '1'\n \n0\#{}\n0")) + assert_equal(" \n0\n1", eval("<<~0 '1'\n \n0\#{}\n0")) end def test_heredoc_mixed_encoding @@ -978,6 +1061,14 @@ eom assert_not_match(/end-of-input/, e.message) end + def test_invalid_regexp + bug20295 = '[ruby-core:116913] [Bug #20295]' + + assert_syntax_error("/[/=~s", /premature end of char-class/, bug20295) + assert_syntax_error("/(?<>)/=~s", /group name is empty/, bug20295) + assert_syntax_error("/(?<a>[)/=~s", /premature end of char-class/, bug20295) + end + def test_lineno_operation_brace_block expected = __LINE__ + 1 actual = caller_lineno\ @@ -990,7 +1081,7 @@ eom ["p ", ""], # no-pop ["", "p Foo::Bar"], # pop ].each do |p1, p2| - src = <<-EOM.gsub(/^\s*\n/, '') + src = <<~EOM class Foo #{"Bar = " + preset if preset} end @@ -1022,7 +1113,7 @@ eom ["p ", ""], # no-pop ["", "p ::Bar"], # pop ].each do |p1, p2| - src = <<-EOM.gsub(/^\s*\n/, '') + src = <<~EOM #{"Bar = " + preset if preset} class Foo #{p1}::Bar #{op}= 42 @@ -1168,11 +1259,71 @@ 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/) end + def test_safe_call_in_for_variable + assert_valid_syntax("for x&.bar in []; end") + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + foo = nil + for foo&.bar in [1]; end + assert_nil(foo) + + foo = Struct.new(:bar).new + for foo&.bar in [1]; end + assert_equal(1, foo.bar) + end; + end + def test_no_warning_logop_literal assert_warning("") do eval("true||raise;nil") @@ -1189,12 +1340,18 @@ eom assert_warn(/string literal in condition/) do eval('1 if ""') end + assert_warning(/string literal in condition/) do + eval('1 if __FILE__') + end assert_warn(/regex literal in condition/) do eval('1 if //') end assert_warning(/literal in condition/) do eval('1 if 1') end + assert_warning(/literal in condition/) do + eval('1 if __LINE__') + end assert_warning(/symbol literal in condition/) do eval('1 if :foo') end @@ -1268,7 +1425,7 @@ eom end def test_parenthesised_statement_argument - assert_syntax_error("foo(bar rescue nil)", /unexpected `rescue' modifier/) + assert_syntax_error("foo(bar rescue nil)", /unexpected 'rescue' modifier/) assert_valid_syntax("foo (bar rescue nil)") end @@ -1290,7 +1447,7 @@ eom def test_block_after_cmdarg_in_paren bug11873 = '[ruby-core:72482] [Bug #11873]' - def bug11873.p(*);end; + def bug11873.p(*, &);end; assert_raise(LocalJumpError, bug11873) do bug11873.instance_eval do @@ -1318,6 +1475,25 @@ eom assert_valid_syntax 'p :foo, {proc do end => proc do end, b: proc do end}', bug13073 end + def test_invalid_encoding_symbol + assert_syntax_error('{"\xC3": 1}', "invalid symbol") + end + + def test_invalid_symbol_in_hash_memory_leak + assert_no_memory_leak([], "#{<<-'begin;'}", "#{<<-'end;'}", rss: true) + str = '{"\xC3": 1}'.force_encoding("UTF-8") + code = proc do + eval(str) + raise "unreachable" + rescue SyntaxError + end + + 1_000.times(&code) + begin; + 1_000_000.times(&code) + end; + end + def test_do_after_local_variable obj = Object.new def obj.m; yield; end @@ -1366,9 +1542,10 @@ eom "#{return}" raise((return; "should not raise")) begin raise; ensure return; end; self - begin raise; ensure return; end and self nil&defined?0--begin e=no_method_error(); return; 0;end return puts('ignored') #=> ignored + BEGIN {return} + END {return if false} end; .split(/\n/).map {|s|[(line+=1), *s.split(/#=> /, 2)]} failed = proc do |n, s| @@ -1398,6 +1575,54 @@ eom end end + def test_eval_return_toplevel + feature4840 = '[ruby-core:36785] [Feature #4840]' + line = __LINE__+2 + code = "#{<<~"begin;"}#{<<~'end;'}" + begin; + eval "return"; raise + begin eval "return"; rescue SystemExit; exit false; end + begin eval "return"; ensure puts "ensured"; end #=> ensured + begin ensure eval "return"; end + begin raise; ensure; eval "return"; end + begin raise; rescue; eval "return"; end + eval "return false"; raise + eval "return 1"; raise + "#{eval "return"}" + raise((eval "return"; "should not raise")) + begin raise; ensure eval "return"; end; self + begin raise; ensure eval "return"; end and self + eval "return puts('ignored')" #=> ignored + BEGIN {eval "return"} + end; + .split(/\n/).map {|s|[(line+=1), *s.split(/#=> /, 2)]} + failed = proc do |n, s| + RubyVM::InstructionSequence.compile(s, __FILE__, nil, n).disasm + end + Tempfile.create(%w"test_return_ .rb") do |lib| + lib.close + args = %W[-W0 -r#{lib.path}] + all_assertions_foreach(feature4840, *[:main, :lib].product([:class, :top], code)) do |main, klass, (n, s, *ex)| + if klass == :class + s = "class X; #{s}; end" + if main == :main + assert_in_out_err(%[-W0], s, ex, /return/, proc {failed[n, s]}, success: false) + else + File.write(lib, s) + assert_in_out_err(args, "", ex, /return/, proc {failed[n, s]}, success: false) + end + else + if main == :main + assert_in_out_err(%[-W0], s, ex, [], proc {failed[n, s]}, success: true) + else + File.write(lib, s) + assert_in_out_err(args, "", ex, [], proc {failed[n, s]}, success: true) + end + end + end + end + end + def test_return_toplevel_with_argument assert_warn(/argument of top-level return is ignored/) {eval("return 1")} end @@ -1406,6 +1631,20 @@ eom assert_in_out_err(['-e', 'class TestSyntax; proc{ return }.call; end'], "", [], /^-e:1:.*unexpected return \(LocalJumpError\)/) end + def test_return_in_END + assert_normal_exit('END {return}') + end + + def test_return_in_BEGIN_in_eval + # `BEGIN` in `eval` is allowed, even inside a method, and `return` + # from that block exits from that method without `LocalJumpError`. + obj = Object.new + def obj.ok + eval("BEGIN {return :ok}") + end + assert_equal :ok, assert_nothing_raised(LocalJumpError) {obj.ok} + end + def test_syntax_error_in_rescue bug12613 = '[ruby-core:76531] [Bug #12613]' assert_syntax_error("#{<<-"begin;"}\n#{<<-"end;"}", /Invalid retry/, bug12613) @@ -1424,13 +1663,13 @@ eom end def test_syntax_error_at_newline - expected = "\n ^" + expected = /(\n|\| ) \^/ assert_syntax_error("%[abcdef", expected) assert_syntax_error("%[abcdef\n", expected) end def test_invalid_jump - assert_in_out_err(%w[-e redo], "", [], /^-e:1: /) + assert_in_out_err(%w[-e redo], "", [], /^-e:1: |~ Invalid redo/) end def test_keyword_not_parens @@ -1601,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 @@ -1625,6 +1861,33 @@ eom def test_command_with_cmd_brace_block assert_valid_syntax('obj.foo (1) {}') assert_valid_syntax('obj::foo (1) {}') + assert_valid_syntax('bar {}') + assert_valid_syntax('Bar {}') + assert_valid_syntax('bar() {}') + assert_valid_syntax('Bar() {}') + assert_valid_syntax('Foo::bar {}') + assert_valid_syntax('Foo::Bar {}') + assert_valid_syntax('Foo::bar() {}') + assert_valid_syntax('Foo::Bar() {}') + end + + def test_command_newline_in_tlparen_args + assert_valid_syntax("p (1\n2\n),(3),(4)") + assert_valid_syntax("p (\n),(),()") + assert_valid_syntax("a.b (1\n2\n),(3),(4)") + assert_valid_syntax("a.b (\n),(),()") + end + + def test_command_semicolon_in_tlparen_at_the_first_arg + bug19281 = '[ruby-core:111499] [Bug #19281]' + assert_valid_syntax('p (1;2),(3),(4)', bug19281) + assert_valid_syntax('p (;),(),()', bug19281) + assert_valid_syntax('a.b (1;2),(3),(4)', bug19281) + assert_valid_syntax('a.b (;),(),()', bug19281) + end + + def test_command_do_block_call_with_empty_args_brace_block + assert_valid_syntax('cmd 1, 2 do end.m() { blk_body }') end def test_numbered_parameter @@ -1655,7 +1918,7 @@ eom assert_syntax_error('def x(_4) end', /_4 is reserved for numbered parameter/) assert_syntax_error('def _5; end', /_5 is reserved for numbered parameter/) assert_syntax_error('def self._6; end', /_6 is reserved for numbered parameter/) - assert_raise_with_message(NameError, /undefined local variable or method `_1'/) { + assert_raise_with_message(NameError, /undefined local variable or method '_1'/) { eval('_1') } ['class C', 'class << C', 'module M', 'def m', 'def o.m'].each do |c| @@ -1670,6 +1933,84 @@ eom assert_raise(NameError) {eval("_1")}, ] } + + assert_valid_syntax("proc {def foo(_);end;_1}") + assert_valid_syntax("p { [_1 **2] }") + assert_valid_syntax("proc {_1;def foo();end;_1}") + end + + def test_it + assert_valid_syntax('proc {it}') + assert_syntax_error('[1,2].then {it+_2}', /'it' is already used/) + assert_syntax_error('[1,2].then {_2+it}', /numbered parameter is already used/) + assert_equal([1, 2], eval('[1,2].then {it}')) + assert_syntax_error('[1,2].then {"#{it}#{_2}"}', /'it' is already used/) + assert_syntax_error('[1,2].then {"#{_2}#{it}"}', /numbered parameter is already used/) + assert_syntax_error('->{it+_2}.call(1,2)', /'it' is already used/) + assert_syntax_error('->{_2+it}.call(1,2)', /numbered parameter is already used/) + assert_equal(4, eval('->(a=->{it}){a}.call.call(4)')) + assert_equal(5, eval('-> a: ->{it} {a}.call.call(5)')) + assert_syntax_error('proc {|| it}', /ordinary parameter is defined/) + assert_syntax_error('proc {|;a| it}', /ordinary parameter is defined/) + assert_syntax_error("proc {|\n| it}", /ordinary parameter is defined/) + assert_syntax_error('proc {|x| it}', /ordinary parameter is defined/) + assert_equal([1, 2], eval('1.then {[it, 2.then {_1}]}')) + assert_equal([2, 1], eval('1.then {[2.then {_1}, it]}')) + assert_syntax_error('->(){it}', /ordinary parameter is defined/) + assert_syntax_error('->(x){it}', /ordinary parameter is defined/) + assert_syntax_error('->x{it}', /ordinary parameter is defined/) + assert_syntax_error('->x:_1{}', /ordinary parameter is defined/) + assert_syntax_error('->x=it{}', /ordinary parameter is defined/) + assert_valid_syntax('-> {it; -> {_2}}') + assert_valid_syntax('-> {-> {it}; _2}') + assert_equal([1, nil], eval('proc {that=it; it=nil; [that, it]}.call(1)')) + assert_equal(1, eval('proc {it = 1}.call')) + assert_warning(/1: warning: assigned but unused variable - it/) { + assert_equal(2, eval('a=Object.new; def a.foo; it = 2; end; a.foo')) + } + assert_equal(3, eval('proc {|it| it}.call(3)')) + assert_equal(4, eval('a=Object.new; def a.foo(it); it; end; a.foo(4)')) + assert_equal(5, eval('a=Object.new; def a.it; 5; end; a.it')) + assert_equal(6, eval('a=Class.new; a.class_eval{ def it; 6; end }; a.new.it')) + assert_equal([7, 8], eval('a=[]; [7].each { a << it; [8].each { a << it } }; a')) + assert_raise_with_message(NameError, /undefined local variable or method 'it'/) do + eval('it') + end + ['class C', 'class << C', 'module M', 'def m', 'def o.m'].each do |c| + assert_valid_syntax("->{#{c};->{it};end;it}\n") + assert_valid_syntax("->{it;#{c};->{it};end}\n") + end + 1.times do + [ + assert_equal(0, it), + assert_equal([:a], eval('[:a].map{it}')), + assert_raise(NameError) {eval('it')}, + ] + 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 @@ -1680,6 +2021,11 @@ eom assert_valid_syntax("tap {a = (break unless true)}") end + def test_value_expr_in_singleton + mesg = /void value expression/ + assert_syntax_error("class << (return); end", mesg) + end + def test_tautological_condition assert_valid_syntax("def f() return if false and invalid; nil end") assert_valid_syntax("def f() return unless true or invalid; nil end") @@ -1697,16 +2043,23 @@ eom assert_valid_syntax("def foo b = 1, ...; bar(...); end") assert_valid_syntax("(def foo ...\n bar(...)\nend)") assert_valid_syntax("(def foo ...; bar(...); end)") + assert_valid_syntax("def (1...).foo ...; bar(...); end") + assert_valid_syntax("def (tap{1...}).foo ...; bar(...); end") assert_valid_syntax('def ==(...) end') assert_valid_syntax('def [](...) end') assert_valid_syntax('def nil(...) end') assert_valid_syntax('def true(...) end') assert_valid_syntax('def false(...) end') + assert_valid_syntax('->a=1...{}') unexpected = /unexpected \.{3}/ assert_syntax_error('iter do |...| end', /unexpected/) assert_syntax_error('iter {|...|}', /unexpected/) assert_syntax_error('->... {}', unexpected) assert_syntax_error('->(...) {}', unexpected) + assert_syntax_error('->a,... {}', unexpected) + assert_syntax_error('->(a,...) {}', unexpected) + assert_syntax_error('->a=1,... {}', unexpected) + assert_syntax_error('->(a=1,...) {}', unexpected) assert_syntax_error('def foo(x, y, z) bar(...); end', /unexpected/) assert_syntax_error('def foo(x, y, z) super(...); end', /unexpected/) assert_syntax_error('def foo(...) yield(...); end', /unexpected/) @@ -1730,9 +2083,11 @@ eom end obj4 = obj1.clone obj5 = obj1.clone + obj6 = obj1.clone obj1.instance_eval('def foo(...) 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) @@ -1761,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}) } @@ -1919,14 +2274,35 @@ eom assert_equal 0...1, exp.call(a: 0) end + def test_argument_forwarding_with_super + assert_valid_syntax('def foo(...) super {}; end') + assert_valid_syntax('def foo(...) super() {}; end') + assert_syntax_error('def foo(...) super(...) {}; end', /both block arg and actual block/) + assert_syntax_error('def foo(...) super(1, ...) {}; end', /both block arg and actual block/) + end + + def test_argument_forwarding_with_super_memory_leak + assert_no_memory_leak([], "#{<<-'begin;'}", "#{<<-'end;'}", rss: true) + code = proc do + eval("def foo(...) super(...) {}; end") + raise "unreachable" + rescue SyntaxError + end + + 1_000.times(&code) + begin; + 100_000.times(&code) + end; + 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 @@ -1949,6 +2325,43 @@ eom RUBY end + def test_defined_in_short_circuit_if_condition + bug = '[ruby-core:20501]' + conds = [ + "false && defined?(Some::CONSTANT)", + "true || defined?(Some::CONSTANT)", + "(false && defined?(Some::CONSTANT))", # parens exercise different code path + "(true || defined?(Some::CONSTANT))", + "@val && false && defined?(Some::CONSTANT)", + "@val || true || defined?(Some::CONSTANT)" + ] + + conds.each do |cond| + code = %Q{ + def my_method + var = "there" + if #{cond} + var = "here" + end + raise var + end + begin + my_method + rescue + print 'ok' + else + print 'ng' + end + } + # Invoke in a subprocess because the bug caused a segfault using the parse.y compiler. + # Don't use assert_separately because the bug was best reproducible in a clean slate, + # without test env loaded. + out, _err, status = EnvUtil.invoke_ruby(["--disable-gems"], code, true, false) + assert_predicate(status, :success?, bug) + assert_equal 'ok', out + end + end + private def not_label(x) @result = x; @not_label ||= nil end @@ -1983,7 +2396,7 @@ eom end end - def caller_lineno(*) + def caller_lineno(*, &) caller_locations(1, 1)[0].lineno end end diff --git a/test/ruby/test_system.rb b/test/ruby/test_system.rb index 31c9cd7654..3fcdaa6aad 100644 --- a/test/ruby/test_system.rb +++ b/test/ruby/test_system.rb @@ -146,6 +146,19 @@ class TestSystem < Test::Unit::TestCase end end + def test_system_closed + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + ios = [] + ObjectSpace.each_object(IO) {|io| ios << io} + `echo` + ObjectSpace.each_object(IO) do |io| + next if ios.include?(io) + assert_nothing_raised {io.close} + end + end; + end + def test_empty_evstr assert_equal("", eval('"#{}"', nil, __FILE__, __LINE__), "[ruby-dev:25113]") end diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index e6f347c486..2a61fc3450 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -243,6 +243,10 @@ class TestThread < Test::Unit::TestCase def test_join_argument_conversion t = Thread.new {} + + # Make sure that the thread terminates + Thread.pass while t.status + assert_raise(TypeError) {t.join(:foo)} limit = Struct.new(:to_f, :count).new(0.05) @@ -323,7 +327,6 @@ class TestThread < Test::Unit::TestCase s += 1 end Thread.pass until t.stop? - sleep 1 if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # t.stop? behaves unexpectedly with --jit-wait assert_equal(1, s) t.wakeup Thread.pass while t.alive? @@ -395,7 +398,7 @@ class TestThread < Test::Unit::TestCase end INPUT - assert_in_out_err(%w(--disable-gems -d), <<-INPUT, %w(false 2), %r".+") + assert_in_out_err(%w(-d), <<-INPUT, %w(false 2), %r".+") p Thread.abort_on_exception begin t = Thread.new { raise } @@ -1434,7 +1437,8 @@ q.pop Thread.pass until th1.stop? # After a thread starts (and execute `sleep`), it returns native_thread_id - assert_instance_of Integer, th1.native_thread_id + native_tid = th1.native_thread_id + assert_instance_of Integer, native_tid if native_tid # it can be nil th1.wakeup Thread.pass while th1.alive? @@ -1443,11 +1447,42 @@ q.pop assert_nil th1.native_thread_id end + def test_thread_native_thread_id_across_fork_on_linux + begin + require '-test-/thread/id' + rescue LoadError + omit "this test is only for Linux" + else + extend Bug::ThreadID + end + + parent_thread_id = Thread.main.native_thread_id + real_parent_thread_id = gettid + + assert_equal real_parent_thread_id, parent_thread_id + + child_lines = nil + IO.popen('-') do |pipe| + if pipe + # parent + child_lines = pipe.read.lines + else + # child + puts Thread.main.native_thread_id + puts gettid + end + end + child_thread_id = child_lines[0].chomp.to_i + real_child_thread_id = child_lines[1].chomp.to_i + + assert_equal real_child_thread_id, child_thread_id + refute_equal parent_thread_id, child_thread_id + end + def test_thread_interrupt_for_killed_thread - opts = { timeout: 5, timeout_error: nil } + pend "hang-up" if /mswin|mingw/ =~ RUBY_PLATFORM - # prevent SIGABRT from slow shutdown with RJIT - opts[:reprieve] = 3 if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? + opts = { timeout: 5, timeout_error: nil } assert_normal_exit(<<-_end, '[Bug #8996]', **opts) Thread.report_on_exception = false @@ -1524,4 +1559,97 @@ q.pop assert_equal(true, t.pending_interrupt?(Exception)) assert_equal(false, t.pending_interrupt?(ArgumentError)) end + + def test_deadlock_backtrace + bug21127 = '[ruby-core:120930] [Bug #21127]' + + expected_stderr = [ + /-:12:in 'Thread#join': No live threads left. Deadlock\? \(fatal\)\n/, + /2 threads, 2 sleeps current:\w+ main thread:\w+\n/, + /\* #<Thread:\w+ sleep_forever>\n/, + :*, + /^\s*-:6:in 'Object#frame_for_deadlock_test_2'/, + :*, + /\* #<Thread:\w+ -:10 sleep_forever>\n/, + :*, + /^\s*-:2:in 'Object#frame_for_deadlock_test_1'/, + :*, + ] + + assert_in_out_err([], <<-INPUT, [], expected_stderr, bug21127) + def frame_for_deadlock_test_1 + yield + end + + def frame_for_deadlock_test_2 + yield + end + + q = Thread::Queue.new + t = Thread.new { frame_for_deadlock_test_1 { 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 709d0ece5c..9a41be8b1a 100644 --- a/test/ruby/test_thread_queue.rb +++ b/test/ruby/test_thread_queue.rb @@ -19,6 +19,15 @@ class TestThreadQueue < Test::Unit::TestCase } end + def test_freeze + assert_raise(TypeError) { + Queue.new.freeze + } + assert_raise(TypeError) { + SizedQueue.new(5).freeze + } + end + def test_queue grind(5, 1000, 15, Queue) end @@ -208,14 +217,14 @@ class TestThreadQueue < Test::Unit::TestCase bug5343 = '[ruby-core:39634]' Dir.mktmpdir {|d| - timeout = EnvUtil.apply_timeout_scale(60) + timeout = 60 total_count = 250 begin - assert_normal_exit(<<-"_eom", bug5343, **{:timeout => timeout, :chdir=>d}) + assert_normal_exit(<<-"_eom", bug5343, timeout: timeout, chdir: d) + r, w = IO.pipe #{total_count}.times do |i| - open("test_thr_kill_count", "w") {|f| f.puts i } + File.open("test_thr_kill_count", "w") {|f| f.puts i } queue = Thread::Queue.new - r, w = IO.pipe th = Thread.start { queue.push(nil) r.read 1 @@ -226,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 @@ -364,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 @@ -418,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 @@ -552,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| @@ -585,9 +600,14 @@ class TestThreadQueue < Test::Unit::TestCase count_items = rand(3000..5000) count_producers = rand(10..20) + # ensure threads do not start running too soon and complete before we check status + mutex = Mutex.new + mutex.lock + producers = count_producers.times.map do Thread.new do - sleep(rand / 100) + mutex.lock + mutex.unlock count_items.times{|i| q << [i,"#{i} for #{Thread.current.inspect}"]} end end @@ -605,9 +625,11 @@ class TestThreadQueue < Test::Unit::TestCase # No dead or finished threads, give up to 10 seconds to start running t = Time.now - Thread.pass until Time.now - t > 10 || (consumers + producers).all?{|thr| thr.status =~ /\A(?:run|sleep)\z/} + Thread.pass until Time.now - t > 10 || (consumers + producers).all?{|thr| thr.status.to_s =~ /\A(?:run|sleep)\z/} + + assert (consumers + producers).all?{|thr| thr.status.to_s =~ /\A(?:run|sleep)\z/}, 'no threads running' - assert (consumers + producers).all?{|thr| thr.status =~ /\A(?:run|sleep)\z/}, 'no threads running' + mutex.unlock # just exercising the concurrency of the support methods. counter = Thread.new do diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index 2bf6056a49..333edb8021 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -75,7 +75,9 @@ class TestTime < Test::Unit::TestCase Time.new("2020-12-25 00 +09:00") } + assert_equal(Time.new(2021), Time.new("2021")) assert_equal(Time.new(2021, 12, 25, in: "+09:00"), Time.new("2021-12-25+09:00")) + assert_equal(Time.new(2021, 12, 25, in: "+09:00"), Time.new("2021-12-25+09:00", in: "-01:00")) assert_equal(0.123456r, Time.new("2021-12-25 00:00:00.123456 +09:00").subsec) assert_equal(0.123456789r, Time.new("2021-12-25 00:00:00.123456789876 +09:00").subsec) @@ -138,6 +140,30 @@ class TestTime < Test::Unit::TestCase assert_raise_with_message(ArgumentError, /mon out of range/) { Time.new("2020-17-25 00:56:17 +0900") } + assert_raise_with_message(ArgumentError, /no time information/) { + Time.new("2020-12") + } + assert_raise_with_message(ArgumentError, /no time information/) { + Time.new("2020-12-02") + } + assert_raise_with_message(ArgumentError, /can't parse/) { + Time.new(" 2020-12-02 00:00:00") + } + assert_raise_with_message(ArgumentError, /can't parse/) { + Time.new("2020-12-02 00:00:00 ") + } + assert_raise_with_message(ArgumentError, /utc_offset/) { + Time.new("2020-12-25 00:00:00 +0960") + } + assert_raise_with_message(ArgumentError, /utc_offset/) { + Time.new("2020-12-25 00:00:00 +09:60") + } + assert_raise_with_message(ArgumentError, /utc_offset/) { + Time.new("2020-12-25 00:00:00 +090060") + } + assert_raise_with_message(ArgumentError, /utc_offset/) { + Time.new("2020-12-25 00:00:00 +09:00:60") + } end def test_time_add() @@ -425,7 +451,7 @@ class TestTime < Test::Unit::TestCase end def test_marshal_zone_gc - assert_separately(%w(--disable-gems), <<-'end;', timeout: 30) + assert_separately([], <<-'end;', timeout: 30) ENV["TZ"] = "JST-9" s = Marshal.dump(Time.now) t = Marshal.load(s) @@ -1394,7 +1420,10 @@ class TestTime < Test::Unit::TestCase def test_memsize # Time objects are common in some code, try to keep them small omit "Time object size test" if /^(?:i.?86|x86_64)-linux/ !~ RUBY_PLATFORM - omit "GC is in debug" if GC::INTERNAL_CONSTANTS[:DEBUG] + omit "GC is in debug" if GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] > 0 + omit "memsize is not accurate due to using malloc_usable_size" if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit "Only run this test on 64-bit" if RbConfig::SIZEOF["void*"] != 8 + require 'objspace' t = Time.at(0) sizeof_timew = @@ -1403,12 +1432,9 @@ class TestTime < Test::Unit::TestCase else RbConfig::SIZEOF["void*"] # Same size as VALUE end - expect = - GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + - sizeof_timew + - RbConfig::SIZEOF["void*"] * 4 + 5 + # vtm - 1 # tzmode, tm_got - assert_equal expect, ObjectSpace.memsize_of(t) + sizeof_vtm = RbConfig::SIZEOF["void*"] * 4 + 8 + expect = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + sizeof_timew + sizeof_vtm + assert_operator ObjectSpace.memsize_of(t), :<=, expect rescue LoadError => e omit "failed to load objspace: #{e.message}" end @@ -1430,4 +1456,63 @@ class TestTime < Test::Unit::TestCase def test_parse_zero_bigint assert_equal 0, Time.new("2020-10-28T16:48:07.000Z").nsec, '[Bug #19390]' end + + def test_xmlschema_encode + [:xmlschema, :iso8601].each do |method| + bug6100 = '[ruby-core:42997]' + + t = Time.utc(2001, 4, 17, 19, 23, 17, 300000) + assert_equal("2001-04-17T19:23:17Z", t.__send__(method)) + assert_equal("2001-04-17T19:23:17.3Z", t.__send__(method, 1)) + assert_equal("2001-04-17T19:23:17.300000Z", t.__send__(method, 6)) + assert_equal("2001-04-17T19:23:17.3000000Z", t.__send__(method, 7)) + assert_equal("2001-04-17T19:23:17.3Z", t.__send__(method, 1.9), bug6100) + + t = Time.utc(2001, 4, 17, 19, 23, 17, 123456) + assert_equal("2001-04-17T19:23:17.1234560Z", t.__send__(method, 7)) + assert_equal("2001-04-17T19:23:17.123456Z", t.__send__(method, 6)) + assert_equal("2001-04-17T19:23:17.12345Z", t.__send__(method, 5)) + assert_equal("2001-04-17T19:23:17.1Z", t.__send__(method, 1)) + assert_equal("2001-04-17T19:23:17.1Z", t.__send__(method, 1.9), bug6100) + + t = Time.at(2.quo(3)).getlocal("+09:00") + assert_equal("1970-01-01T09:00:00.666+09:00", t.__send__(method, 3)) + assert_equal("1970-01-01T09:00:00.6666666666+09:00", t.__send__(method, 10)) + assert_equal("1970-01-01T09:00:00.66666666666666666666+09:00", t.__send__(method, 20)) + assert_equal("1970-01-01T09:00:00.6+09:00", t.__send__(method, 1.1), bug6100) + assert_equal("1970-01-01T09:00:00.666+09:00", t.__send__(method, 3.2), bug6100) + + t = Time.at(123456789.quo(9999999999)).getlocal("+09:00") + assert_equal("1970-01-01T09:00:00.012+09:00", t.__send__(method, 3)) + assert_equal("1970-01-01T09:00:00.012345678+09:00", t.__send__(method, 9)) + assert_equal("1970-01-01T09:00:00.0123456789+09:00", t.__send__(method, 10)) + assert_equal("1970-01-01T09:00:00.0123456789012345678+09:00", t.__send__(method, 19)) + assert_equal("1970-01-01T09:00:00.01234567890123456789+09:00", t.__send__(method, 20)) + assert_equal("1970-01-01T09:00:00.012+09:00", t.__send__(method, 3.8), bug6100) + + t = Time.utc(1) + assert_equal("0001-01-01T00:00:00Z", t.__send__(method)) + + begin + Time.at(-1) + rescue ArgumentError + # ignore + else + t = Time.utc(1960, 12, 31, 23, 0, 0, 123456) + assert_equal("1960-12-31T23:00:00.123456Z", t.__send__(method, 6)) + end + + t = get_t2000.getlocal("-09:30") # Pacific/Marquesas + assert_equal("1999-12-31T14:30:00-09:30", t.__send__(method)) + + assert_equal("10000-01-01T00:00:00Z", Time.utc(10000).__send__(method)) + assert_equal("9999-01-01T00:00:00Z", Time.utc(9999).__send__(method)) + assert_equal("0001-01-01T00:00:00Z", Time.utc(1).__send__(method)) # 1 AD + assert_equal("0000-01-01T00:00:00Z", Time.utc(0).__send__(method)) # 1 BC + assert_equal("-0001-01-01T00:00:00Z", Time.utc(-1).__send__(method)) # 2 BC + assert_equal("-0004-01-01T00:00:00Z", Time.utc(-4).__send__(method)) # 5 BC + assert_equal("-9999-01-01T00:00:00Z", Time.utc(-9999).__send__(method)) + assert_equal("-10000-01-01T00:00:00Z", Time.utc(-10000).__send__(method)) + end + end end diff --git a/test/ruby/test_time_tz.rb b/test/ruby/test_time_tz.rb index 6e7e813923..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 @@ -202,7 +201,7 @@ class TestTimeTZ < Test::Unit::TestCase def test_europe_lisbon with_tz("Europe/Lisbon") { - assert_equal("LMT", Time.new(-0x1_0000_0000_0000_0000).zone) + assert_include(%w"LMT CET", Time.new(-0x1_0000_0000_0000_0000).zone) } end if has_lisbon_tz @@ -695,6 +694,13 @@ module TestTimeTZ::WithTZ assert_equal(t.dst?, t2.dst?) end + def subtest_fractional_second(time_class, tz, tzarg, tzname, abbr, utc_offset) + t = time_class.new(2024, 1, 1, 23, 59, 59.9r, tzarg) + assert_equal(utc_offset[t.dst? ? 1 : 0], t.utc_offset) + t = time_class.new(2024, 7, 1, 23, 59, 59.9r, tzarg) + assert_equal(utc_offset[t.dst? ? 1 : 0], t.utc_offset) + end + def test_invalid_zone make_timezone("INVALID", "INV", 0) rescue => e @@ -719,6 +725,7 @@ module TestTimeTZ::WithTZ "Asia/Tokyo" => ["JST", +9*3600], "America/Los_Angeles" => ["PST", -8*3600, "PDT", -7*3600], "Africa/Ndjamena" => ["WAT", +1*3600], + "Etc/UTC" => ["UTC", 0], } def make_timezone(tzname, abbr, utc_offset, abbr2 = nil, utc_offset2 = nil) diff --git a/test/ruby/test_transcode.rb b/test/ruby/test_transcode.rb index 24ee9b9533..99b5ee8d43 100644 --- a/test/ruby/test_transcode.rb +++ b/test/ruby/test_transcode.rb @@ -10,9 +10,9 @@ class TestTranscode < Test::Unit::TestCase assert_raise(Encoding::ConverterNotFoundError) { 'abc'.encode!('foo', 'bar') } assert_raise(Encoding::ConverterNotFoundError) { 'abc'.force_encoding('utf-8').encode('foo') } assert_raise(Encoding::ConverterNotFoundError) { 'abc'.force_encoding('utf-8').encode!('foo') } - assert_raise(Encoding::UndefinedConversionError) { "\x80".encode('utf-8','ASCII-8BIT') } - assert_raise(Encoding::InvalidByteSequenceError) { "\x80".encode('utf-8','US-ASCII') } - assert_raise(Encoding::UndefinedConversionError) { "\xA5".encode('utf-8','iso-8859-3') } + assert_undefined_in("\x80", 'ASCII-8BIT') + assert_invalid_in("\x80", 'US-ASCII') + assert_undefined_in("\xA5", 'iso-8859-3') assert_raise(FrozenError) { 'hello'.freeze.encode!('iso-8859-1') } assert_raise(FrozenError) { '\u3053\u3093\u306b\u3061\u306f'.freeze.encode!('iso-8859-1') } # ã“ã‚“ã«ã¡ã¯ end @@ -52,16 +52,6 @@ class TestTranscode < Test::Unit::TestCase assert_equal("\u20AC"*200000, ("\xA4"*200000).encode!('utf-8', 'iso-8859-15')) end - def check_both_ways(utf8, raw, encoding) - assert_equal(utf8.force_encoding('utf-8'), raw.encode('utf-8', encoding),utf8.dump+raw.dump) - assert_equal(raw.force_encoding(encoding), utf8.encode(encoding, 'utf-8')) - end - - def check_both_ways2(str1, enc1, str2, enc2) - assert_equal(str1.force_encoding(enc1), str2.encode(enc1, enc2)) - assert_equal(str2.force_encoding(enc2), str1.encode(enc2, enc1)) - end - def test_encoding_of_ascii_originating_from_binary binary_string = [0x82, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x76, 0x65, 0x72, 0x79, 0x20, 0x6c, 0x6f, @@ -188,16 +178,16 @@ class TestTranscode < Test::Unit::TestCase def test_windows_874 check_both_ways("\u20AC", "\x80", 'windows-874') # € - assert_raise(Encoding::UndefinedConversionError) { "\x81".encode("utf-8", 'windows-874') } - assert_raise(Encoding::UndefinedConversionError) { "\x84".encode("utf-8", 'windows-874') } + assert_undefined_in("\x81", 'windows-874') + assert_undefined_in("\x84", 'windows-874') check_both_ways("\u2026", "\x85", 'windows-874') # … - assert_raise(Encoding::UndefinedConversionError) { "\x86".encode("utf-8", 'windows-874') } - assert_raise(Encoding::UndefinedConversionError) { "\x8F".encode("utf-8", 'windows-874') } - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'windows-874') } + assert_undefined_in("\x86", 'windows-874') + assert_undefined_in("\x8F", 'windows-874') + assert_undefined_in("\x90", 'windows-874') check_both_ways("\u2018", "\x91", 'windows-874') # ‘ check_both_ways("\u2014", "\x97", 'windows-874') # — - assert_raise(Encoding::UndefinedConversionError) { "\x98".encode("utf-8", 'windows-874') } - assert_raise(Encoding::UndefinedConversionError) { "\x9F".encode("utf-8", 'windows-874') } + assert_undefined_in("\x98", 'windows-874') + assert_undefined_in("\x9F", 'windows-874') check_both_ways("\u00A0", "\xA0", 'windows-874') # non-breaking space check_both_ways("\u0E0F", "\xAF", 'windows-874') # ภcheck_both_ways("\u0E10", "\xB0", 'windows-874') # ภ@@ -206,31 +196,31 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u0E2F", "\xCF", 'windows-874') # ฯ check_both_ways("\u0E30", "\xD0", 'windows-874') # ะ check_both_ways("\u0E3A", "\xDA", 'windows-874') # ฺ - assert_raise(Encoding::UndefinedConversionError) { "\xDB".encode("utf-8", 'windows-874') } - assert_raise(Encoding::UndefinedConversionError) { "\xDE".encode("utf-8", 'windows-874') } + assert_undefined_in("\xDB", 'windows-874') + assert_undefined_in("\xDE", 'windows-874') check_both_ways("\u0E3F", "\xDF", 'windows-874') # ฿ check_both_ways("\u0E40", "\xE0", 'windows-874') # เ check_both_ways("\u0E4F", "\xEF", 'windows-874') # ๠check_both_ways("\u0E50", "\xF0", 'windows-874') # ๠check_both_ways("\u0E5B", "\xFB", 'windows-874') # ๛ - assert_raise(Encoding::UndefinedConversionError) { "\xFC".encode("utf-8", 'windows-874') } - assert_raise(Encoding::UndefinedConversionError) { "\xFF".encode("utf-8", 'windows-874') } + assert_undefined_in("\xFC", 'windows-874') + assert_undefined_in("\xFF", 'windows-874') end def test_windows_1250 check_both_ways("\u20AC", "\x80", 'windows-1250') # € - assert_raise(Encoding::UndefinedConversionError) { "\x81".encode("utf-8", 'windows-1250') } + assert_undefined_in("\x81", 'windows-1250') check_both_ways("\u201A", "\x82", 'windows-1250') # ‚ - assert_raise(Encoding::UndefinedConversionError) { "\x83".encode("utf-8", 'windows-1250') } + assert_undefined_in("\x83", 'windows-1250') check_both_ways("\u201E", "\x84", 'windows-1250') # „ check_both_ways("\u2021", "\x87", 'windows-1250') # ‡ - assert_raise(Encoding::UndefinedConversionError) { "\x88".encode("utf-8", 'windows-1250') } + assert_undefined_in("\x88", 'windows-1250') check_both_ways("\u2030", "\x89", 'windows-1250') # ‰ check_both_ways("\u0179", "\x8F", 'windows-1250') # Ź - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'windows-1250') } + assert_undefined_in("\x90", 'windows-1250') check_both_ways("\u2018", "\x91", 'windows-1250') # ‘ check_both_ways("\u2014", "\x97", 'windows-1250') # — - assert_raise(Encoding::UndefinedConversionError) { "\x98".encode("utf-8", 'windows-1250') } + assert_undefined_in("\x98", 'windows-1250') check_both_ways("\u2122", "\x99", 'windows-1250') # â„¢ check_both_ways("\u00A0", "\xA0", 'windows-1250') # non-breaking space check_both_ways("\u017B", "\xAF", 'windows-1250') # Å» @@ -251,7 +241,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u20AC", "\x88", 'windows-1251') # € check_both_ways("\u040F", "\x8F", 'windows-1251') # Ð check_both_ways("\u0452", "\x90", 'windows-1251') # Ñ’ - assert_raise(Encoding::UndefinedConversionError) { "\x98".encode("utf-8", 'windows-1251') } + assert_undefined_in("\x98", 'windows-1251') check_both_ways("\u045F", "\x9F", 'windows-1251') # ÑŸ check_both_ways("\u00A0", "\xA0", 'windows-1251') # non-breaking space check_both_ways("\u0407", "\xAF", 'windows-1251') # Ї @@ -269,16 +259,16 @@ class TestTranscode < Test::Unit::TestCase def test_windows_1252 check_both_ways("\u20AC", "\x80", 'windows-1252') # € - assert_raise(Encoding::UndefinedConversionError) { "\x81".encode("utf-8", 'windows-1252') } + assert_undefined_in("\x81", 'windows-1252') check_both_ways("\u201A", "\x82", 'windows-1252') # ‚ check_both_ways("\u0152", "\x8C", 'windows-1252') # >Å’ - assert_raise(Encoding::UndefinedConversionError) { "\x8D".encode("utf-8", 'windows-1252') } + assert_undefined_in("\x8D", 'windows-1252') check_both_ways("\u017D", "\x8E", 'windows-1252') # Ž - assert_raise(Encoding::UndefinedConversionError) { "\x8F".encode("utf-8", 'windows-1252') } - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'windows-1252') } + assert_undefined_in("\x8F", 'windows-1252') + assert_undefined_in("\x90", 'windows-1252') check_both_ways("\u2018", "\x91", 'windows-1252') #‘ check_both_ways("\u0153", "\x9C", 'windows-1252') # Å“ - assert_raise(Encoding::UndefinedConversionError) { "\x9D".encode("utf-8", 'windows-1252') } + assert_undefined_in("\x9D", 'windows-1252') check_both_ways("\u017E", "\x9E", 'windows-1252') # ž check_both_ways("\u00A0", "\xA0", 'windows-1252') # non-breaking space check_both_ways("\u00AF", "\xAF", 'windows-1252') # ¯ @@ -296,24 +286,24 @@ class TestTranscode < Test::Unit::TestCase def test_windows_1253 check_both_ways("\u20AC", "\x80", 'windows-1253') # € - assert_raise(Encoding::UndefinedConversionError) { "\x81".encode("utf-8", 'windows-1253') } + assert_undefined_in("\x81", 'windows-1253') check_both_ways("\u201A", "\x82", 'windows-1253') # ‚ check_both_ways("\u2021", "\x87", 'windows-1253') # ‡ - assert_raise(Encoding::UndefinedConversionError) { "\x88".encode("utf-8", 'windows-1253') } + assert_undefined_in("\x88", 'windows-1253') check_both_ways("\u2030", "\x89", 'windows-1253') # ‰ - assert_raise(Encoding::UndefinedConversionError) { "\x8A".encode("utf-8", 'windows-1253') } + assert_undefined_in("\x8A", 'windows-1253') check_both_ways("\u2039", "\x8B", 'windows-1253') # ‹ - assert_raise(Encoding::UndefinedConversionError) { "\x8C".encode("utf-8", 'windows-1253') } - assert_raise(Encoding::UndefinedConversionError) { "\x8F".encode("utf-8", 'windows-1253') } - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'windows-1253') } + assert_undefined_in("\x8C", 'windows-1253') + assert_undefined_in("\x8F", 'windows-1253') + assert_undefined_in("\x90", 'windows-1253') check_both_ways("\u2018", "\x91", 'windows-1253') # ‘ check_both_ways("\u2014", "\x97", 'windows-1253') # — - assert_raise(Encoding::UndefinedConversionError) { "\x98".encode("utf-8", 'windows-1253') } + assert_undefined_in("\x98", 'windows-1253') check_both_ways("\u2122", "\x99", 'windows-1253') # â„¢ - assert_raise(Encoding::UndefinedConversionError) { "\x9A".encode("utf-8", 'windows-1253') } + assert_undefined_in("\x9A", 'windows-1253') check_both_ways("\u203A", "\x9B", 'windows-1253') # › - assert_raise(Encoding::UndefinedConversionError) { "\x9C".encode("utf-8", 'windows-1253') } - assert_raise(Encoding::UndefinedConversionError) { "\x9F".encode("utf-8", 'windows-1253') } + assert_undefined_in("\x9C", 'windows-1253') + assert_undefined_in("\x9F", 'windows-1253') check_both_ways("\u00A0", "\xA0", 'windows-1253') # non-breaking space check_both_ways("\u2015", "\xAF", 'windows-1253') # ― check_both_ways("\u00B0", "\xB0", 'windows-1253') # ° @@ -322,28 +312,28 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u039F", "\xCF", 'windows-1253') # Ο check_both_ways("\u03A0", "\xD0", 'windows-1253') # Î check_both_ways("\u03A1", "\xD1", 'windows-1253') # Ρ - assert_raise(Encoding::UndefinedConversionError) { "\xD2".encode("utf-8", 'windows-1253') } + assert_undefined_in("\xD2", 'windows-1253') check_both_ways("\u03A3", "\xD3", 'windows-1253') # Σ check_both_ways("\u03AF", "\xDF", 'windows-1253') # ί check_both_ways("\u03B0", "\xE0", 'windows-1253') # ΰ check_both_ways("\u03BF", "\xEF", 'windows-1253') # ο check_both_ways("\u03C0", "\xF0", 'windows-1253') # Ï€ check_both_ways("\u03CE", "\xFE", 'windows-1253') # ÏŽ - assert_raise(Encoding::UndefinedConversionError) { "\xFF".encode("utf-8", 'windows-1253') } + assert_undefined_in("\xFF", 'windows-1253') end def test_windows_1254 check_both_ways("\u20AC", "\x80", 'windows-1254') # € - assert_raise(Encoding::UndefinedConversionError) { "\x81".encode("utf-8", 'windows-1254') } + assert_undefined_in("\x81", 'windows-1254') check_both_ways("\u201A", "\x82", 'windows-1254') # ‚ check_both_ways("\u0152", "\x8C", 'windows-1254') # Å’ - assert_raise(Encoding::UndefinedConversionError) { "\x8D".encode("utf-8", 'windows-1254') } - assert_raise(Encoding::UndefinedConversionError) { "\x8F".encode("utf-8", 'windows-1254') } - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'windows-1254') } + assert_undefined_in("\x8D", 'windows-1254') + assert_undefined_in("\x8F", 'windows-1254') + assert_undefined_in("\x90", 'windows-1254') check_both_ways("\u2018", "\x91", 'windows-1254') # ‘ check_both_ways("\u0153", "\x9C", 'windows-1254') # Å“ - assert_raise(Encoding::UndefinedConversionError) { "\x9D".encode("utf-8", 'windows-1254') } - assert_raise(Encoding::UndefinedConversionError) { "\x9E".encode("utf-8", 'windows-1254') } + assert_undefined_in("\x9D", 'windows-1254') + assert_undefined_in("\x9E", 'windows-1254') check_both_ways("\u0178", "\x9F", 'windows-1254') # Ÿ check_both_ways("\u00A0", "\xA0", 'windows-1254') # non-breaking space check_both_ways("\u00AF", "\xAF", 'windows-1254') # ¯ @@ -361,20 +351,20 @@ class TestTranscode < Test::Unit::TestCase def test_windows_1255 check_both_ways("\u20AC", "\x80", 'windows-1255') # € - assert_raise(Encoding::UndefinedConversionError) { "\x81".encode("utf-8", 'windows-1255') } + assert_undefined_in("\x81", 'windows-1255') check_both_ways("\u201A", "\x82", 'windows-1255') # ‚ check_both_ways("\u2030", "\x89", 'windows-1255') # ‰ - assert_raise(Encoding::UndefinedConversionError) { "\x8A".encode("utf-8", 'windows-1255') } + assert_undefined_in("\x8A", 'windows-1255') check_both_ways("\u2039", "\x8B", 'windows-1255') # ‹ - assert_raise(Encoding::UndefinedConversionError) { "\x8C".encode("utf-8", 'windows-1255') } - assert_raise(Encoding::UndefinedConversionError) { "\x8F".encode("utf-8", 'windows-1255') } - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'windows-1255') } + assert_undefined_in("\x8C", 'windows-1255') + assert_undefined_in("\x8F", 'windows-1255') + assert_undefined_in("\x90", 'windows-1255') check_both_ways("\u2018", "\x91", 'windows-1255') # ‘ check_both_ways("\u2122", "\x99", 'windows-1255') # â„¢ - assert_raise(Encoding::UndefinedConversionError) { "\x9A".encode("utf-8", 'windows-1255') } + assert_undefined_in("\x9A", 'windows-1255') check_both_ways("\u203A", "\x9B", 'windows-1255') # › - assert_raise(Encoding::UndefinedConversionError) { "\x9C".encode("utf-8", 'windows-1255') } - assert_raise(Encoding::UndefinedConversionError) { "\x9F".encode("utf-8", 'windows-1255') } + assert_undefined_in("\x9C", 'windows-1255') + assert_undefined_in("\x9F", 'windows-1255') check_both_ways("\u00A0", "\xA0", 'windows-1255') # non-breaking space check_both_ways("\u00A1", "\xA1", 'windows-1255') # ¡ check_both_ways("\u00D7", "\xAA", 'windows-1255') # × @@ -391,17 +381,17 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u05C0", "\xD0", 'windows-1255') # ×€ check_both_ways("\u05F3", "\xD7", 'windows-1255') # ׳ check_both_ways("\u05F4", "\xD8", 'windows-1255') # ×´ - assert_raise(Encoding::UndefinedConversionError) { "\xD9".encode("utf-8", 'windows-1255') } - assert_raise(Encoding::UndefinedConversionError) { "\xDF".encode("utf-8", 'windows-1255') } + assert_undefined_in("\xD9", 'windows-1255') + assert_undefined_in("\xDF", 'windows-1255') check_both_ways("\u05D0", "\xE0", 'windows-1255') # × check_both_ways("\u05DF", "\xEF", 'windows-1255') # ן check_both_ways("\u05E0", "\xF0", 'windows-1255') # × check_both_ways("\u05EA", "\xFA", 'windows-1255') # ת - assert_raise(Encoding::UndefinedConversionError) { "\xFB".encode("utf-8", 'windows-1255') } - assert_raise(Encoding::UndefinedConversionError) { "\xFC".encode("utf-8", 'windows-1255') } + assert_undefined_in("\xFB", 'windows-1255') + assert_undefined_in("\xFC", 'windows-1255') check_both_ways("\u200E", "\xFD", 'windows-1255') # left-to-right mark check_both_ways("\u200F", "\xFE", 'windows-1255') # right-to-left mark - assert_raise(Encoding::UndefinedConversionError) { "\xFF".encode("utf-8", 'windows-1255') } + assert_undefined_in("\xFF", 'windows-1255') end def test_windows_1256 @@ -429,35 +419,35 @@ class TestTranscode < Test::Unit::TestCase def test_windows_1257 check_both_ways("\u20AC", "\x80", 'windows-1257') # € - assert_raise(Encoding::UndefinedConversionError) { "\x81".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x81", 'windows-1257') check_both_ways("\u201A", "\x82", 'windows-1257') # ‚ - assert_raise(Encoding::UndefinedConversionError) { "\x83".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x83", 'windows-1257') check_both_ways("\u201E", "\x84", 'windows-1257') # „ check_both_ways("\u2021", "\x87", 'windows-1257') # ‡ - assert_raise(Encoding::UndefinedConversionError) { "\x88".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x88", 'windows-1257') check_both_ways("\u2030", "\x89", 'windows-1257') # ‰ - assert_raise(Encoding::UndefinedConversionError) { "\x8A".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x8A", 'windows-1257') check_both_ways("\u2039", "\x8B", 'windows-1257') # ‹ - assert_raise(Encoding::UndefinedConversionError) { "\x8C".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x8C", 'windows-1257') check_both_ways("\u00A8", "\x8D", 'windows-1257') # ¨ check_both_ways("\u02C7", "\x8E", 'windows-1257') # ˇ check_both_ways("\u00B8", "\x8F", 'windows-1257') # ¸ - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x90", 'windows-1257') check_both_ways("\u2018", "\x91", 'windows-1257') # ‘ check_both_ways("\u2014", "\x97", 'windows-1257') # — - assert_raise(Encoding::UndefinedConversionError) { "\x98".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x98", 'windows-1257') check_both_ways("\u2122", "\x99", 'windows-1257') # â„¢ - assert_raise(Encoding::UndefinedConversionError) { "\x9A".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x9A", 'windows-1257') check_both_ways("\u203A", "\x9B", 'windows-1257') # › - assert_raise(Encoding::UndefinedConversionError) { "\x9C".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x9C", 'windows-1257') check_both_ways("\u00AF", "\x9D", 'windows-1257') # ¯ check_both_ways("\u02DB", "\x9E", 'windows-1257') # Ë› - assert_raise(Encoding::UndefinedConversionError) { "\x9F".encode("utf-8", 'windows-1257') } + assert_undefined_in("\x9F", 'windows-1257') check_both_ways("\u00A0", "\xA0", 'windows-1257') # non-breaking space - assert_raise(Encoding::UndefinedConversionError) { "\xA1".encode("utf-8", 'windows-1257') } + assert_undefined_in("\xA1", 'windows-1257') check_both_ways("\u00A2", "\xA2", 'windows-1257') # ¢ check_both_ways("\u00A4", "\xA4", 'windows-1257') # ¤ - assert_raise(Encoding::UndefinedConversionError) { "\xA5".encode("utf-8", 'windows-1257') } + assert_undefined_in("\xA5", 'windows-1257') check_both_ways("\u00A6", "\xA6", 'windows-1257') # ¦ check_both_ways("\u00C6", "\xAF", 'windows-1257') # Æ check_both_ways("\u00B0", "\xB0", 'windows-1257') # ° @@ -492,9 +482,9 @@ class TestTranscode < Test::Unit::TestCase end def test_IBM720 - assert_raise(Encoding::UndefinedConversionError) { "\x80".encode("utf-8", 'IBM720') } - assert_raise(Encoding::UndefinedConversionError) { "\x8F".encode("utf-8", 'IBM720') } - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'IBM720') } + assert_undefined_in("\x80", 'IBM720') + assert_undefined_in("\x8F", 'IBM720') + assert_undefined_in("\x90", 'IBM720') check_both_ways("\u0627", "\x9F", 'IBM720') # ا check_both_ways("\u0628", "\xA0", 'IBM720') # ب check_both_ways("\u00BB", "\xAF", 'IBM720') # » @@ -580,17 +570,17 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u00A4", "\xCF", 'IBM857') # ¤ check_both_ways("\u00BA", "\xD0", 'IBM857') # º check_both_ways("\u00C8", "\xD4", 'IBM857') # È - assert_raise(Encoding::UndefinedConversionError) { "\xD5".encode("utf-8", 'IBM857') } + assert_undefined_in("\xD5", 'IBM857') check_both_ways("\u00CD", "\xD6", 'IBM857') # à check_both_ways("\u2580", "\xDF", 'IBM857') # â–€ check_both_ways("\u00D3", "\xE0", 'IBM857') # Ó check_both_ways("\u00B5", "\xE6", 'IBM857') # µ - assert_raise(Encoding::UndefinedConversionError) { "\xE7".encode("utf-8", 'IBM857') } + assert_undefined_in("\xE7", 'IBM857') check_both_ways("\u00D7", "\xE8", 'IBM857') # × check_both_ways("\u00B4", "\xEF", 'IBM857') # ´ check_both_ways("\u00AD", "\xF0", 'IBM857') # soft hyphen check_both_ways("\u00B1", "\xF1", 'IBM857') # ± - assert_raise(Encoding::UndefinedConversionError) { "\xF2".encode("utf-8", 'IBM857') } + assert_undefined_in("\xF2", 'IBM857') check_both_ways("\u00BE", "\xF3", 'IBM857') # ¾ check_both_ways("\u00A0", "\xFF", 'IBM857') # non-breaking space end @@ -671,6 +661,25 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u00A0", "\xFF", 'IBM863') # non-breaking space end + def test_IBM864 + check_both_ways("\u00B0", "\x80", 'IBM864') # ° + check_both_ways("\u2518", "\x8F", 'IBM864') # ┘ + check_both_ways("\u03B2", "\x90", 'IBM864') # β + check_both_ways("\uFE73", "\x9F", 'IBM864') # ï¹³ + check_both_ways("\u00A0", "\xA0", 'IBM864') # non-breaking space + check_both_ways("\uFEA5", "\xAF", 'IBM864') # ﺥ + check_both_ways("\u0660", "\xB0", 'IBM864') # Ù + check_both_ways("\u061F", "\xBF", 'IBM864') # ØŸ + check_both_ways("\u00A2", "\xC0", 'IBM864') # ¢ + check_both_ways("\uFEA9", "\xCF", 'IBM864') # ﺩ + check_both_ways("\uFEAB", "\xD0", 'IBM864') # ﺫ + check_both_ways("\uFEC9", "\xDF", 'IBM864') # ﻉ + check_both_ways("\u0640", "\xE0", 'IBM864') # Ù€ + check_both_ways("\uFEE1", "\xEF", 'IBM864') # ﻡ + check_both_ways("\uFE7D", "\xF0", 'IBM864') # ï¹½ + check_both_ways("\u25A0", "\xFE", 'IBM864') # â– + end + def test_IBM865 check_both_ways("\u00C7", "\x80", 'IBM865') # Ç check_both_ways("\u00C5", "\x8F", 'IBM865') # Ã… @@ -710,16 +719,16 @@ class TestTranscode < Test::Unit::TestCase end def test_IBM869 - assert_raise(Encoding::UndefinedConversionError) { "\x80".encode("utf-8", 'IBM869') } - assert_raise(Encoding::UndefinedConversionError) { "\x85".encode("utf-8", 'IBM869') } + assert_undefined_in("\x80", 'IBM869') + assert_undefined_in("\x85", 'IBM869') check_both_ways("\u0386", "\x86", 'IBM869') # Ά - assert_raise(Encoding::UndefinedConversionError) { "\x87".encode("utf-8", 'IBM869') } + assert_undefined_in("\x87", 'IBM869') check_both_ways("\u00B7", "\x88", 'IBM869') # · check_both_ways("\u0389", "\x8F", 'IBM869') # Ή check_both_ways("\u038A", "\x90", 'IBM869') # Ί check_both_ways("\u038C", "\x92", 'IBM869') # ÎŒ - assert_raise(Encoding::UndefinedConversionError) { "\x93".encode("utf-8", 'IBM869') } - assert_raise(Encoding::UndefinedConversionError) { "\x94".encode("utf-8", 'IBM869') } + assert_undefined_in("\x93", 'IBM869') + assert_undefined_in("\x94", 'IBM869') check_both_ways("\u038E", "\x95", 'IBM869') # ÎŽ check_both_ways("\u03AF", "\x9F", 'IBM869') # ί check_both_ways("\u03CA", "\xA0", 'IBM869') # ÏŠ @@ -808,7 +817,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u03BF", "\xEF", 'macGreek') # ο check_both_ways("\u03C0", "\xF0", 'macGreek') # Ï€ check_both_ways("\u03B0", "\xFE", 'macGreek') # ΰ - assert_raise(Encoding::UndefinedConversionError) { "\xFF".encode("utf-8", 'macGreek') } + assert_undefined_in("\xFF", 'macGreek') end def test_macIceland @@ -887,7 +896,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u00D4", "\xEF", 'macTurkish') # Ô #check_both_ways("\uF8FF", "\xF0", 'macTurkish') # Apple logo check_both_ways("\u00D9", "\xF4", 'macTurkish') # Ù - assert_raise(Encoding::UndefinedConversionError) { "\xF5".encode("utf-8", 'macTurkish') } + assert_undefined_in("\xF5", 'macTurkish') check_both_ways("\u02C6", "\xF6", 'macTurkish') # ˆ check_both_ways("\u02C7", "\xFF", 'macTurkish') # ˇ end @@ -958,11 +967,11 @@ class TestTranscode < Test::Unit::TestCase end def test_TIS_620 - assert_raise(Encoding::UndefinedConversionError) { "\x80".encode("utf-8", 'TIS-620') } - assert_raise(Encoding::UndefinedConversionError) { "\x8F".encode("utf-8", 'TIS-620') } - assert_raise(Encoding::UndefinedConversionError) { "\x90".encode("utf-8", 'TIS-620') } - assert_raise(Encoding::UndefinedConversionError) { "\x9F".encode("utf-8", 'TIS-620') } - assert_raise(Encoding::UndefinedConversionError) { "\xA0".encode("utf-8", 'TIS-620') } + assert_undefined_in("\x80", 'TIS-620') + assert_undefined_in("\x8F", 'TIS-620') + assert_undefined_in("\x90", 'TIS-620') + assert_undefined_in("\x9F", 'TIS-620') + assert_undefined_in("\xA0", 'TIS-620') check_both_ways("\u0E01", "\xA1", 'TIS-620') # ภcheck_both_ways("\u0E0F", "\xAF", 'TIS-620') # ภcheck_both_ways("\u0E10", "\xB0", 'TIS-620') # ภ@@ -971,15 +980,15 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u0E2F", "\xCF", 'TIS-620') # ฯ check_both_ways("\u0E30", "\xD0", 'TIS-620') # ะ check_both_ways("\u0E3A", "\xDA", 'TIS-620') # ฺ - assert_raise(Encoding::UndefinedConversionError) { "\xDB".encode("utf-8", 'TIS-620') } - assert_raise(Encoding::UndefinedConversionError) { "\xDE".encode("utf-8", 'TIS-620') } + assert_undefined_in("\xDB", 'TIS-620') + assert_undefined_in("\xDE", 'TIS-620') check_both_ways("\u0E3F", "\xDF", 'TIS-620') # ฿ check_both_ways("\u0E40", "\xE0", 'TIS-620') # เ check_both_ways("\u0E4F", "\xEF", 'TIS-620') # ๠check_both_ways("\u0E50", "\xF0", 'TIS-620') # ๠check_both_ways("\u0E5B", "\xFB", 'TIS-620') # ๛ - assert_raise(Encoding::UndefinedConversionError) { "\xFC".encode("utf-8", 'TIS-620') } - assert_raise(Encoding::UndefinedConversionError) { "\xFF".encode("utf-8", 'TIS-620') } + assert_undefined_in("\xFC", 'TIS-620') + assert_undefined_in("\xFF", 'TIS-620') end def test_CP850 @@ -1182,15 +1191,15 @@ class TestTranscode < Test::Unit::TestCase expected = "\u{3042}\u{3044}\u{20bb7}" assert_equal(expected, %w/fffe4230443042d8b7df/.pack("H*").encode("UTF-8","UTF-16")) check_both_ways(expected, %w/feff30423044d842dfb7/.pack("H*"), "UTF-16") - assert_raise(Encoding::InvalidByteSequenceError){%w/feffdfb7/.pack("H*").encode("UTF-8","UTF-16")} - assert_raise(Encoding::InvalidByteSequenceError){%w/fffeb7df/.pack("H*").encode("UTF-8","UTF-16")} + assert_invalid_in(%w/feffdfb7/.pack("H*"), "UTF-16") + assert_invalid_in(%w/fffeb7df/.pack("H*"), "UTF-16") end def test_utf_32_bom expected = "\u{3042}\u{3044}\u{20bb7}" assert_equal(expected, %w/fffe00004230000044300000b70b0200/.pack("H*").encode("UTF-8","UTF-32")) check_both_ways(expected, %w/0000feff000030420000304400020bb7/.pack("H*"), "UTF-32") - assert_raise(Encoding::InvalidByteSequenceError){%w/0000feff00110000/.pack("H*").encode("UTF-8","UTF-32")} + assert_invalid_in(%w/0000feff00110000/.pack("H*"), "UTF-32") end def check_utf_32_both_ways(utf8, raw) @@ -1372,24 +1381,24 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u71FC", "\xE0\x9E", 'shift_jis') # 燼 check_both_ways("\u71F9", "\xE0\x9F", 'shift_jis') # 燹 check_both_ways("\u73F1", "\xE0\xFC", 'shift_jis') # ç± - assert_raise(Encoding::UndefinedConversionError) { "\xEF\x40".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xEF\x7E".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xEF\x80".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xEF\x9E".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xEF\x9F".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xEF\xFC".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xF0\x40".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xF0\x7E".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xF0\x80".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xF0\x9E".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xF0\x9F".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xF0\xFC".encode("utf-8", 'shift_jis') } + assert_undefined_in("\xEF\x40", 'shift_jis') + assert_undefined_in("\xEF\x7E", 'shift_jis') + assert_undefined_in("\xEF\x80", 'shift_jis') + assert_undefined_in("\xEF\x9E", 'shift_jis') + assert_undefined_in("\xEF\x9F", 'shift_jis') + assert_undefined_in("\xEF\xFC", 'shift_jis') + assert_undefined_in("\xF0\x40", 'shift_jis') + assert_undefined_in("\xF0\x7E", 'shift_jis') + assert_undefined_in("\xF0\x80", 'shift_jis') + assert_undefined_in("\xF0\x9E", 'shift_jis') + assert_undefined_in("\xF0\x9F", 'shift_jis') + assert_undefined_in("\xF0\xFC", 'shift_jis') #check_both_ways("\u9ADC", "\xFC\x40", 'shift_jis') # 髜 (IBM extended) - assert_raise(Encoding::UndefinedConversionError) { "\xFC\x7E".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xFC\x80".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xFC\x9E".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xFC\x9F".encode("utf-8", 'shift_jis') } - assert_raise(Encoding::UndefinedConversionError) { "\xFC\xFC".encode("utf-8", 'shift_jis') } + assert_undefined_in("\xFC\x7E", 'shift_jis') + assert_undefined_in("\xFC\x80", 'shift_jis') + assert_undefined_in("\xFC\x9E", 'shift_jis') + assert_undefined_in("\xFC\x9F", 'shift_jis') + assert_undefined_in("\xFC\xFC", 'shift_jis') check_both_ways("\u677E\u672C\u884C\u5F18", "\x8f\xbc\x96\x7b\x8d\x73\x8d\x4f", 'shift_jis') # æ¾æœ¬è¡Œå¼˜ check_both_ways("\u9752\u5C71\u5B66\u9662\u5927\u5B66", "\x90\xC2\x8E\x52\x8A\x77\x89\x40\x91\xE5\x8A\x77", 'shift_jis') # é’å±±å¦é™¢å¤§å¦ check_both_ways("\u795E\u6797\u7FA9\u535A", "\x90\x5F\x97\xD1\x8B\x60\x94\x8E", 'shift_jis') # ç¥žæž—ç¾©åš @@ -1409,34 +1418,34 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u00F7", "\xA1\xE0", 'euc-jp') # ÷ check_both_ways("\u25C7", "\xA1\xFE", 'euc-jp') # â—‡ check_both_ways("\u25C6", "\xA2\xA1", 'euc-jp') # â—† - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xAF".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xB9".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xC2".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xC9".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xD1".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xDB".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xEB".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xF1".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xFA".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xFD".encode("utf-8", 'euc-jp') } + assert_undefined_in("\xA2\xAF", 'euc-jp') + assert_undefined_in("\xA2\xB9", 'euc-jp') + assert_undefined_in("\xA2\xC2", 'euc-jp') + assert_undefined_in("\xA2\xC9", 'euc-jp') + assert_undefined_in("\xA2\xD1", 'euc-jp') + assert_undefined_in("\xA2\xDB", 'euc-jp') + assert_undefined_in("\xA2\xEB", 'euc-jp') + assert_undefined_in("\xA2\xF1", 'euc-jp') + assert_undefined_in("\xA2\xFA", 'euc-jp') + assert_undefined_in("\xA2\xFD", 'euc-jp') check_both_ways("\u25EF", "\xA2\xFE", 'euc-jp') # â—¯ - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xAF".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xBA".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xC0".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xDB".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xE0".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xFB".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA4\xF4".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA5\xF7".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA6\xB9".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA6\xC0".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA6\xD9".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA7\xC2".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA7\xD0".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA7\xF2".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xA8\xC1".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xCF\xD4".encode("utf-8", 'euc-jp') } - assert_raise(Encoding::UndefinedConversionError) { "\xCF\xFE".encode("utf-8", 'euc-jp') } + assert_undefined_in("\xA3\xAF", 'euc-jp') + assert_undefined_in("\xA3\xBA", 'euc-jp') + assert_undefined_in("\xA3\xC0", 'euc-jp') + assert_undefined_in("\xA3\xDB", 'euc-jp') + assert_undefined_in("\xA3\xE0", 'euc-jp') + assert_undefined_in("\xA3\xFB", 'euc-jp') + assert_undefined_in("\xA4\xF4", 'euc-jp') + assert_undefined_in("\xA5\xF7", 'euc-jp') + assert_undefined_in("\xA6\xB9", 'euc-jp') + assert_undefined_in("\xA6\xC0", 'euc-jp') + assert_undefined_in("\xA6\xD9", 'euc-jp') + assert_undefined_in("\xA7\xC2", 'euc-jp') + assert_undefined_in("\xA7\xD0", 'euc-jp') + assert_undefined_in("\xA7\xF2", 'euc-jp') + assert_undefined_in("\xA8\xC1", 'euc-jp') + assert_undefined_in("\xCF\xD4", 'euc-jp') + assert_undefined_in("\xCF\xFE", 'euc-jp') check_both_ways("\u6A97", "\xDD\xA1", 'euc-jp') # 檗 check_both_ways("\u6BEF", "\xDD\xDF", 'euc-jp') # 毯 check_both_ways("\u9EBE", "\xDD\xE0", 'euc-jp') # 麾 @@ -1449,7 +1458,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u71FC", "\xDF\xFE", 'euc-jp') # 燼 check_both_ways("\u71F9", "\xE0\xA1", 'euc-jp') # 燹 check_both_ways("\u73F1", "\xE0\xFE", 'euc-jp') # ç± - assert_raise(Encoding::UndefinedConversionError) { "\xF4\xA7".encode("utf-8", 'euc-jp') } + assert_undefined_in("\xF4\xA7", 'euc-jp') #check_both_ways("\u9ADC", "\xFC\xE3", 'euc-jp') # 髜 (IBM extended) check_both_ways("\u677E\u672C\u884C\u5F18", "\xBE\xBE\xCB\xDC\xB9\xD4\xB9\xB0", 'euc-jp') # æ¾æœ¬è¡Œå¼˜ @@ -1481,7 +1490,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u2127", "\xA3\xE0", 'euc-jis-2004') # â„§ check_both_ways("\u30A0", "\xA3\xFB", 'euc-jis-2004') # ã‚ check_both_ways("\uFF54", "\xA3\xF4", 'euc-jis-2004') # ï½” - assert_raise(Encoding::UndefinedConversionError) { "\xA5\xF7".encode("utf-8", 'euc-jis-2004') } + assert_undefined_in("\xA5\xF7", 'euc-jis-2004') check_both_ways("\u2664", "\xA6\xB9", 'euc-jis-2004') # ♤ check_both_ways("\u2663", "\xA6\xC0", 'euc-jis-2004') # ♣ check_both_ways("\u03C2", "\xA6\xD9", 'euc-jis-2004') # Ï‚ @@ -1566,33 +1575,33 @@ class TestTranscode < Test::Unit::TestCase end def test_eucjp_sjis_undef - assert_raise(Encoding::UndefinedConversionError) { "\x8e\xe0".encode("Shift_JIS", "EUC-JP") } - assert_raise(Encoding::UndefinedConversionError) { "\x8e\xfe".encode("Shift_JIS", "EUC-JP") } - assert_raise(Encoding::UndefinedConversionError) { "\x8f\xa1\xa1".encode("Shift_JIS", "EUC-JP") } - assert_raise(Encoding::UndefinedConversionError) { "\x8f\xa1\xfe".encode("Shift_JIS", "EUC-JP") } - assert_raise(Encoding::UndefinedConversionError) { "\x8f\xfe\xa1".encode("Shift_JIS", "EUC-JP") } - assert_raise(Encoding::UndefinedConversionError) { "\x8f\xfe\xfe".encode("Shift_JIS", "EUC-JP") } - - assert_raise(Encoding::UndefinedConversionError) { "\xf0\x40".encode("EUC-JP", "Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\xf0\x7e".encode("EUC-JP", "Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\xf0\x80".encode("EUC-JP", "Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\xf0\xfc".encode("EUC-JP", "Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\xfc\x40".encode("EUC-JP", "Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\xfc\x7e".encode("EUC-JP", "Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\xfc\x80".encode("EUC-JP", "Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\xfc\xfc".encode("EUC-JP", "Shift_JIS") } + assert_undefined_conversion("\x8e\xe0", "Shift_JIS", "EUC-JP") + assert_undefined_conversion("\x8e\xfe", "Shift_JIS", "EUC-JP") + assert_undefined_conversion("\x8f\xa1\xa1", "Shift_JIS", "EUC-JP") + assert_undefined_conversion("\x8f\xa1\xfe", "Shift_JIS", "EUC-JP") + assert_undefined_conversion("\x8f\xfe\xa1", "Shift_JIS", "EUC-JP") + assert_undefined_conversion("\x8f\xfe\xfe", "Shift_JIS", "EUC-JP") + + assert_undefined_conversion("\xf0\x40", "EUC-JP", "Shift_JIS") + assert_undefined_conversion("\xf0\x7e", "EUC-JP", "Shift_JIS") + assert_undefined_conversion("\xf0\x80", "EUC-JP", "Shift_JIS") + assert_undefined_conversion("\xf0\xfc", "EUC-JP", "Shift_JIS") + assert_undefined_conversion("\xfc\x40", "EUC-JP", "Shift_JIS") + assert_undefined_conversion("\xfc\x7e", "EUC-JP", "Shift_JIS") + assert_undefined_conversion("\xfc\x80", "EUC-JP", "Shift_JIS") + assert_undefined_conversion("\xfc\xfc", "EUC-JP", "Shift_JIS") end def test_iso_2022_jp - assert_raise(Encoding::InvalidByteSequenceError) { "\x1b(A".encode("utf-8", "iso-2022-jp") } - assert_raise(Encoding::InvalidByteSequenceError) { "\x1b$(A".encode("utf-8", "iso-2022-jp") } - assert_raise(Encoding::InvalidByteSequenceError) { "\x1b$C".encode("utf-8", "iso-2022-jp") } - assert_raise(Encoding::InvalidByteSequenceError) { "\x0e".encode("utf-8", "iso-2022-jp") } - assert_raise(Encoding::InvalidByteSequenceError) { "\x80".encode("utf-8", "iso-2022-jp") } - assert_raise(Encoding::InvalidByteSequenceError) { "\x1b$(Dd!\x1b(B".encode("utf-8", "iso-2022-jp") } - assert_raise(Encoding::UndefinedConversionError) { "\u9299".encode("iso-2022-jp") } - assert_raise(Encoding::UndefinedConversionError) { "\uff71\uff72\uff73\uff74\uff75".encode("iso-2022-jp") } - assert_raise(Encoding::InvalidByteSequenceError) { "\x1b(I12345\x1b(B".encode("utf-8", "iso-2022-jp") } + assert_invalid_in("\x1b(A", "iso-2022-jp") + assert_invalid_in("\x1b$(A", "iso-2022-jp") + assert_invalid_in("\x1b$C", "iso-2022-jp") + assert_invalid_in("\x0e", "iso-2022-jp") + assert_invalid_in("\x80", "iso-2022-jp") + assert_invalid_in("\x1b$(Dd!\x1b(B", "iso-2022-jp") + assert_undefined_conversion("\u9299", "iso-2022-jp") + assert_undefined_conversion("\uff71\uff72\uff73\uff74\uff75", "iso-2022-jp") + assert_invalid_in("\x1b(I12345\x1b(B", "iso-2022-jp") assert_equal("\xA1\xA1".force_encoding("euc-jp"), "\e$B!!\e(B".encode("EUC-JP", "ISO-2022-JP")) assert_equal("\e$B!!\e(B".force_encoding("ISO-2022-JP"), @@ -1625,6 +1634,8 @@ class TestTranscode < Test::Unit::TestCase assert_equal("\e$B%*!+%,%I%J!+%N!+%P%\\%^!+%Q%]%\"\e(B".force_encoding("cp50220"), "\xB5\xDE\xB6\xDE\xC4\xDE\xC5\xDE\xC9\xDE\xCA\xDE\xCE\xDE\xCF\xDE\xCA\xDF\xCE\xDF\xB1". encode("cp50220", "sjis")) + assert_equal("\e$B\x21\x23\e(I\x7E\e(B".force_encoding("cp50220"), + "\x8E\xA1\x8E\xFE".encode("cp50220", "cp51932")) end def test_iso_2022_jp_1 @@ -1655,11 +1666,11 @@ class TestTranscode < Test::Unit::TestCase assert_equal("\u005C", "\e(J\x5C\e(B".encode("UTF-8", "ISO-2022-JP")) assert_equal("\u005C", "\x5C".encode("stateless-ISO-2022-JP", "ISO-2022-JP")) assert_equal("\u005C", "\e(J\x5C\e(B".encode("stateless-ISO-2022-JP", "ISO-2022-JP")) - assert_raise(Encoding::UndefinedConversionError) { "\u00A5".encode("Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\u00A5".encode("Windows-31J") } - assert_raise(Encoding::UndefinedConversionError) { "\u00A5".encode("EUC-JP") } - assert_raise(Encoding::UndefinedConversionError) { "\u00A5".encode("eucJP-ms") } - assert_raise(Encoding::UndefinedConversionError) { "\u00A5".encode("CP51932") } + assert_undefined_conversion("\u00A5", "Shift_JIS") + assert_undefined_conversion("\u00A5", "Windows-31J") + assert_undefined_conversion("\u00A5", "EUC-JP") + assert_undefined_conversion("\u00A5", "eucJP-ms") + assert_undefined_conversion("\u00A5", "CP51932") # FULLWIDTH REVERSE SOLIDUS check_both_ways("\uFF3C", "\x81\x5F", "Shift_JIS") @@ -1680,21 +1691,21 @@ class TestTranscode < Test::Unit::TestCase assert_equal("\u007E", "\e(J\x7E\e(B".encode("UTF-8", "ISO-2022-JP")) assert_equal("\u007E", "\x7E".encode("stateless-ISO-2022-JP", "ISO-2022-JP")) assert_equal("\u007E", "\e(J\x7E\e(B".encode("stateless-ISO-2022-JP", "ISO-2022-JP")) - assert_raise(Encoding::UndefinedConversionError) { "\u203E".encode("Shift_JIS") } - assert_raise(Encoding::UndefinedConversionError) { "\u203E".encode("Windows-31J") } - assert_raise(Encoding::UndefinedConversionError) { "\u203E".encode("EUC-JP") } - assert_raise(Encoding::UndefinedConversionError) { "\u203E".encode("eucJP-ms") } - assert_raise(Encoding::UndefinedConversionError) { "\u203E".encode("CP51932") } + assert_undefined_conversion("\u203E", "Shift_JIS") + assert_undefined_conversion("\u203E", "Windows-31J") + assert_undefined_conversion("\u203E", "EUC-JP") + assert_undefined_conversion("\u203E", "eucJP-ms") + assert_undefined_conversion("\u203E", "CP51932") end def test_gb2312 check_both_ways("\u3000", "\xA1\xA1", 'GB2312') # full-width space check_both_ways("\u3013", "\xA1\xFE", 'GB2312') # 〓 - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xB0".encode("utf-8", 'GB2312') } + assert_undefined_in("\xA2\xB0", 'GB2312') check_both_ways("\u2488", "\xA2\xB1", 'GB2312') # â’ˆ - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xE4".encode("utf-8", 'GB2312') } + assert_undefined_in("\xA2\xE4", 'GB2312') check_both_ways("\u3220", "\xA2\xE5", 'GB2312') # ㈠- assert_raise(Encoding::UndefinedConversionError) { "\xA2\xF0".encode("utf-8", 'GB2312') } + assert_undefined_in("\xA2\xF0", 'GB2312') check_both_ways("\u2160", "\xA2\xF1", 'GB2312') # â… check_both_ways("\uFF01", "\xA3\xA1", 'GB2312') # ï¼ check_both_ways("\uFFE3", "\xA3\xFE", 'GB2312') # ï¿£ @@ -1705,9 +1716,9 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u0410", "\xA7\xA1", 'GB2312') # Ð check_both_ways("\u0430", "\xA7\xD1", 'GB2312') # а check_both_ways("\u0101", "\xA8\xA1", 'GB2312') # Ä - assert_raise(Encoding::UndefinedConversionError) { "\xA8\xC4".encode("utf-8", 'GB2312') } + assert_undefined_in("\xA8\xC4", 'GB2312') check_both_ways("\u3105", "\xA8\xC5", 'GB2312') # ã„… - assert_raise(Encoding::UndefinedConversionError) { "\xA9\xA3".encode("utf-8", 'GB2312') } + assert_undefined_in("\xA9\xA3", 'GB2312') check_both_ways("\u2500", "\xA9\xA4", 'GB2312') # ─ check_both_ways("\u554A", "\xB0\xA1", 'GB2312') # 啊 check_both_ways("\u5265", "\xB0\xFE", 'GB2312') # 剥 @@ -1721,7 +1732,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u7384", "\xD0\xFE", 'GB2312') # 玄 check_both_ways("\u4F4F", "\xD7\xA1", 'GB2312') # ä½ check_both_ways("\u5EA7", "\xD7\xF9", 'GB2312') # 座 - assert_raise(Encoding::UndefinedConversionError) { "\xD7\xFA".encode("utf-8", 'GB2312') } + assert_undefined_in("\xD7\xFA", 'GB2312') check_both_ways("\u647A", "\xDF\xA1", 'GB2312') # 摺 check_both_ways("\u553C", "\xDF\xFE", 'GB2312') # 唼 check_both_ways("\u5537", "\xE0\xA1", 'GB2312') # å”· @@ -1759,48 +1770,48 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u3000", "\xA1\xA1", 'GBK') # full-width space check_both_ways("\u3001", "\xA1\xA2", 'GBK') # 〠check_both_ways("\u3013", "\xA1\xFE", 'GBK') # 〓 - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xA0".encode("utf-8", 'GBK') } + assert_undefined_in("\xA2\xA0", 'GBK') check_both_ways("\u2170", "\xA2\xA1", 'GBK') # â…° - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xB0".encode("utf-8", 'GBK') } + assert_undefined_in("\xA2\xB0", 'GBK') check_both_ways("\u2488", "\xA2\xB1", 'GBK') # â’ˆ - assert_raise(Encoding::UndefinedConversionError) { "\xA2\xE4".encode("utf-8", 'GBK') } + assert_undefined_in("\xA2\xE4", 'GBK') check_both_ways("\u3220", "\xA2\xE5", 'GBK') # ㈠- assert_raise(Encoding::UndefinedConversionError) { "\xA2\xF0".encode("utf-8", 'GBK') } + assert_undefined_in("\xA2\xF0", 'GBK') check_both_ways("\u2160", "\xA2\xF1", 'GBK') # â… - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xA0".encode("utf-8", 'GBK') } + assert_undefined_in("\xA3\xA0", 'GBK') check_both_ways("\uFF01", "\xA3\xA1", 'GBK') # ï¼ check_both_ways("\uFFE3", "\xA3\xFE", 'GBK') # ï¿£ - assert_raise(Encoding::UndefinedConversionError) { "\xA4\xA0".encode("utf-8", 'GBK') } + assert_undefined_in("\xA4\xA0", 'GBK') check_both_ways("\u3041", "\xA4\xA1", 'GBK') # ã - assert_raise(Encoding::UndefinedConversionError) { "\xA5\xA0".encode("utf-8", 'GBK') } + assert_undefined_in("\xA5\xA0", 'GBK') check_both_ways("\u30A1", "\xA5\xA1", 'GBK') # ã‚¡ check_both_ways("\u0391", "\xA6\xA1", 'GBK') # Α check_both_ways("\u03B1", "\xA6\xC1", 'GBK') # α - assert_raise(Encoding::UndefinedConversionError) { "\xA6\xED".encode("utf-8", 'GBK') } + assert_undefined_in("\xA6\xED", 'GBK') check_both_ways("\uFE3B", "\xA6\xEE", 'GBK') # ︻ check_both_ways("\u0410", "\xA7\xA1", 'GBK') # Ð check_both_ways("\u0430", "\xA7\xD1", 'GBK') # а check_both_ways("\u02CA", "\xA8\x40", 'GBK') # ËŠ check_both_ways("\u2587", "\xA8\x7E", 'GBK') # â–‡ - assert_raise(Encoding::UndefinedConversionError) { "\xA8\x96".encode("utf-8", 'GBK') } + assert_undefined_in("\xA8\x96", 'GBK') check_both_ways("\u0101", "\xA8\xA1", 'GBK') # Ä - assert_raise(Encoding::UndefinedConversionError) { "\xA8\xBC".encode("utf-8", 'GBK') } - assert_raise(Encoding::UndefinedConversionError) { "\xA8\xBF".encode("utf-8", 'GBK') } - assert_raise(Encoding::UndefinedConversionError) { "\xA8\xC4".encode("utf-8", 'GBK') } + assert_undefined_in("\xA8\xBC", 'GBK') + assert_undefined_in("\xA8\xBF", 'GBK') + assert_undefined_in("\xA8\xC4", 'GBK') check_both_ways("\u3105", "\xA8\xC5", 'GBK') # ã„… check_both_ways("\u3021", "\xA9\x40", 'GBK') # 〡 - assert_raise(Encoding::UndefinedConversionError) { "\xA9\x58".encode("utf-8", 'GBK') } - assert_raise(Encoding::UndefinedConversionError) { "\xA9\x5B".encode("utf-8", 'GBK') } - assert_raise(Encoding::UndefinedConversionError) { "\xA9\x5D".encode("utf-8", 'GBK') } + assert_undefined_in("\xA9\x58", 'GBK') + assert_undefined_in("\xA9\x5B", 'GBK') + assert_undefined_in("\xA9\x5D", 'GBK') check_both_ways("\u3007", "\xA9\x96", 'GBK') # 〇 - assert_raise(Encoding::UndefinedConversionError) { "\xA9\xA3".encode("utf-8", 'GBK') } + assert_undefined_in("\xA9\xA3", 'GBK') check_both_ways("\u2500", "\xA9\xA4", 'GBK') # ─ - assert_raise(Encoding::UndefinedConversionError) { "\xA9\xF0".encode("utf-8", 'GBK') } + assert_undefined_in("\xA9\xF0", 'GBK') check_both_ways("\u7588", "\xAF\x40", 'GBK') # ç–ˆ check_both_ways("\u7607", "\xAF\x7E", 'GBK') # 瘇 check_both_ways("\u7608", "\xAF\x80", 'GBK') # 瘈 check_both_ways("\u7644", "\xAF\xA0", 'GBK') # 癄 - assert_raise(Encoding::UndefinedConversionError) { "\xAF\xA1".encode("utf-8", 'GBK') } + assert_undefined_in("\xAF\xA1", 'GBK') check_both_ways("\u7645", "\xB0\x40", 'GBK') # ç™… check_both_ways("\u769B", "\xB0\x7E", 'GBK') # çš› check_both_ways("\u769C", "\xB0\x80", 'GBK') # çšœ @@ -1841,10 +1852,10 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u9F78", "\xFD\x7E", 'GBK') # 齸 check_both_ways("\u9F79", "\xFD\x80", 'GBK') # é½¹ check_both_ways("\uF9F1", "\xFD\xA0", 'GBK') # ï§± - assert_raise(Encoding::UndefinedConversionError) { "\xFD\xA1".encode("utf-8", 'GBK') } + assert_undefined_in("\xFD\xA1", 'GBK') check_both_ways("\uFA0C", "\xFE\x40", 'GBK') # 兀 check_both_ways("\uFA29", "\xFE\x4F", 'GBK') # 﨩 - assert_raise(Encoding::UndefinedConversionError) { "\xFE\x50".encode("utf-8", 'GBK') } + assert_undefined_in("\xFE\x50", 'GBK') check_both_ways("\u9752\u5C71\u5B66\u9662\u5927\u5B66", "\xC7\xE0\xC9\xBD\xD1\xA7\xD4\xBA\xB4\xF3\xD1\xA7", 'GBK') # é’å±±å¦é™¢å¤§å¦ check_both_ways("\u795E\u6797\u7FA9\u535A", "\xC9\xF1\xC1\xD6\xC1\x78\xB2\xA9", 'GBK') # ç¥žæž—ç¾©åš end @@ -1880,48 +1891,48 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u3000", "\xA1\xA1", 'GB18030') # full-width space check_both_ways("\u3001", "\xA1\xA2", 'GB18030') # check_both_ways("\u3013", "\xA1\xFE", 'GB18030') # - #assert_raise(Encoding::UndefinedConversionError) { "\xA2\xA0".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA2\xA0", 'GB18030') check_both_ways("\u2170", "\xA2\xA1", 'GB18030') # â…° - #assert_raise(Encoding::UndefinedConversionError) { "\xA2\xB0".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA2\xB0", 'GB18030') check_both_ways("\u2488", "\xA2\xB1", 'GB18030') # - #assert_raise(Encoding::UndefinedConversionError) { "\xA2\xE4".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA2\xE4", 'GB18030') check_both_ways("\u3220", "\xA2\xE5", 'GB18030') # ㈠- #assert_raise(Encoding::UndefinedConversionError) { "\xA2\xF0".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA2\xF0", 'GB18030') check_both_ways("\u2160", "\xA2\xF1", 'GB18030') # â… - #assert_raise(Encoding::UndefinedConversionError) { "\xA3\xA0".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA3\xA0", 'GB18030') check_both_ways("\uFF01", "\xA3\xA1", 'GB18030') # E check_both_ways("\uFFE3", "\xA3\xFE", 'GB18030') # E - #assert_raise(Encoding::UndefinedConversionError) { "\xA4\xA0".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA4\xA0", 'GB18030') check_both_ways("\u3041", "\xA4\xA1", 'GB18030') # - #assert_raise(Encoding::UndefinedConversionError) { "\xA5\xA0".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA5\xA0", 'GB18030') check_both_ways("\u30A1", "\xA5\xA1", 'GB18030') # ã‚¡ check_both_ways("\u0391", "\xA6\xA1", 'GB18030') # check_both_ways("\u03B1", "\xA6\xC1", 'GB18030') # α - #assert_raise(Encoding::UndefinedConversionError) { "\xA6\xED".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA6\xED", 'GB18030') check_both_ways("\uFE3B", "\xA6\xEE", 'GB18030') # E check_both_ways("\u0410", "\xA7\xA1", 'GB18030') # check_both_ways("\u0430", "\xA7\xD1", 'GB18030') # а check_both_ways("\u02CA", "\xA8\x40", 'GB18030') # check_both_ways("\u2587", "\xA8\x7E", 'GB18030') # - #assert_raise(Encoding::UndefinedConversionError) { "\xA8\x96".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA8\x96", 'GB18030') check_both_ways("\u0101", "\xA8\xA1", 'GB18030') # - #assert_raise(Encoding::UndefinedConversionError) { "\xA8\xBC".encode("utf-8", 'GB18030') } - #assert_raise(Encoding::UndefinedConversionError) { "\xA8\xBF".encode("utf-8", 'GB18030') } - #assert_raise(Encoding::UndefinedConversionError) { "\xA8\xC4".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA8\xBC", 'GB18030') + #assert_undefined_in("\xA8\xBF", 'GB18030') + #assert_undefined_in("\xA8\xC4", 'GB18030') check_both_ways("\u3105", "\xA8\xC5", 'GB18030') # check_both_ways("\u3021", "\xA9\x40", 'GB18030') # 〡 - #assert_raise(Encoding::UndefinedConversionError) { "\xA9\x58".encode("utf-8", 'GB18030') } - #assert_raise(Encoding::UndefinedConversionError) { "\xA9\x5B".encode("utf-8", 'GB18030') } - #assert_raise(Encoding::UndefinedConversionError) { "\xA9\x5D".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA9\x58", 'GB18030') + #assert_undefined_in("\xA9\x5B", 'GB18030') + #assert_undefined_in("\xA9\x5D", 'GB18030') check_both_ways("\u3007", "\xA9\x96", 'GB18030') # - #assert_raise(Encoding::UndefinedConversionError) { "\xA9\xA3".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA9\xA3", 'GB18030') check_both_ways("\u2500", "\xA9\xA4", 'GB18030') # ─ - #assert_raise(Encoding::UndefinedConversionError) { "\xA9\xF0".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xA9\xF0", 'GB18030') check_both_ways("\u7588", "\xAF\x40", 'GB18030') # check_both_ways("\u7607", "\xAF\x7E", 'GB18030') # check_both_ways("\u7608", "\xAF\x80", 'GB18030') # check_both_ways("\u7644", "\xAF\xA0", 'GB18030') # - #assert_raise(Encoding::UndefinedConversionError) { "\xAF\xA1".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xAF\xA1", 'GB18030') check_both_ways("\u7645", "\xB0\x40", 'GB18030') # check_both_ways("\u769B", "\xB0\x7E", 'GB18030') # check_both_ways("\u769C", "\xB0\x80", 'GB18030') # @@ -1962,10 +1973,10 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u9F78", "\xFD\x7E", 'GB18030') # 齸 check_both_ways("\u9F79", "\xFD\x80", 'GB18030') # é½¹ check_both_ways("\uF9F1", "\xFD\xA0", 'GB18030') # E - #assert_raise(Encoding::UndefinedConversionError) { "\xFD\xA1".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xFD\xA1", 'GB18030') check_both_ways("\uFA0C", "\xFE\x40", 'GB18030') # E check_both_ways("\uFA29", "\xFE\x4F", 'GB18030') # E - #assert_raise(Encoding::UndefinedConversionError) { "\xFE\x50".encode("utf-8", 'GB18030') } + #assert_undefined_in("\xFE\x50", 'GB18030') check_both_ways("\u9752\u5C71\u5B66\u9662\u5927\u5B66", "\xC7\xE0\xC9\xBD\xD1\xA7\xD4\xBA\xB4\xF3\xD1\xA7", 'GB18030') # é’å±±å¦é™¢å¤§å¦ check_both_ways("\u795E\u6797\u7FA9\u535A", "\xC9\xF1\xC1\xD6\xC1\x78\xB2\xA9", 'GB18030') # 神林義 @@ -2020,7 +2031,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u310F", "\xA3\x7E", 'Big5') # ã„ check_both_ways("\u3110", "\xA3\xA1", 'Big5') # ã„ check_both_ways("\u02CB", "\xA3\xBF", 'Big5') # Ë‹ - assert_raise(Encoding::UndefinedConversionError) { "\xA3\xC0".encode("utf-8", 'Big5') } + assert_undefined_in("\xA3\xC0", 'Big5') check_both_ways("\u6D6C", "\xAF\x40", 'Big5') # 浬 check_both_ways("\u7837", "\xAF\x7E", 'Big5') # ç · check_both_ways("\u7825", "\xAF\xA1", 'Big5') # ç ¥ @@ -2039,9 +2050,9 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u77AC", "\xC0\xFE", 'Big5') # 瞬 check_both_ways("\u8B96", "\xC6\x40", 'Big5') # è®– check_both_ways("\u7C72", "\xC6\x7E", 'Big5') # ç±² - #assert_raise(Encoding::UndefinedConversionError) { "\xC6\xA1".encode("utf-8", 'Big5') } - #assert_raise(Encoding::UndefinedConversionError) { "\xC7\x40".encode("utf-8", 'Big5') } - #assert_raise(Encoding::UndefinedConversionError) { "\xC8\x40".encode("utf-8", 'Big5') } + #assert_undefined_in("\xC6\xA1", 'Big5') + #assert_undefined_in("\xC7\x40", 'Big5') + #assert_undefined_in("\xC8\x40", 'Big5') check_both_ways("\u4E42", "\xC9\x40", 'Big5') # 乂 check_both_ways("\u6C15", "\xC9\x7E", 'Big5') # æ°• check_both_ways("\u6C36", "\xC9\xA1", 'Big5') # æ°¶ @@ -2074,7 +2085,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u9F0A", "\xF9\x7E", 'Big5') # 鼊 check_both_ways("\u9FA4", "\xF9\xA1", 'Big5') # 龤 check_both_ways("\u9F98", "\xF9\xD5", 'Big5') # 龘 - #assert_raise(Encoding::UndefinedConversionError) { "\xF9\xD6".encode("utf-8", 'Big5') } + #assert_undefined_in("\xF9\xD6", 'Big5') check_both_ways("\u795E\u6797\u7FA9\u535A", "\xAF\xAB\xAA\x4C\xB8\x71\xB3\xD5", 'Big5') # ç¥žæž—ç¾©åš end @@ -2087,7 +2098,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u310F", "\xA3\x7E", 'Big5-HKSCS') # ã„ check_both_ways("\u3110", "\xA3\xA1", 'Big5-HKSCS') # ã„ check_both_ways("\u02CB", "\xA3\xBF", 'Big5-HKSCS') # Ë‹ - #assert_raise(Encoding::UndefinedConversionError) { "\xA3\xC0".encode("utf-8", 'Big5-HKSCS') } + #assert_undefined_in("\xA3\xC0", 'Big5-HKSCS') check_both_ways("\u6D6C", "\xAF\x40", 'Big5-HKSCS') # 浬 check_both_ways("\u7837", "\xAF\x7E", 'Big5-HKSCS') # ç · check_both_ways("\u7825", "\xAF\xA1", 'Big5-HKSCS') # ç ¥ @@ -2106,9 +2117,9 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u77AC", "\xC0\xFE", 'Big5-HKSCS') # 瞬 check_both_ways("\u8B96", "\xC6\x40", 'Big5-HKSCS') # è®– check_both_ways("\u7C72", "\xC6\x7E", 'Big5-HKSCS') # ç±² - #assert_raise(Encoding::UndefinedConversionError) { "\xC6\xA1".encode("utf-8", 'Big5-HKSCS') } - #assert_raise(Encoding::UndefinedConversionError) { "\xC7\x40".encode("utf-8", 'Big5-HKSCS') } - #assert_raise(Encoding::UndefinedConversionError) { "\xC8\x40".encode("utf-8", 'Big5-HKSCS') } + #assert_undefined_in("\xC6\xA1", 'Big5-HKSCS') + #assert_undefined_in("\xC7\x40", 'Big5-HKSCS') + #assert_undefined_in("\xC8\x40", 'Big5-HKSCS') check_both_ways("\u4E42", "\xC9\x40", 'Big5-HKSCS') # 乂 check_both_ways("\u6C15", "\xC9\x7E", 'Big5-HKSCS') # æ°• check_both_ways("\u6C36", "\xC9\xA1", 'Big5-HKSCS') # æ°¶ @@ -2142,7 +2153,7 @@ class TestTranscode < Test::Unit::TestCase check_both_ways("\u9FA4", "\xF9\xA1", 'Big5-HKSCS') # 龤 check_both_ways("\u9F98", "\xF9\xD5", 'Big5-HKSCS') # 龘 #check_both_ways("\u{23ED7}", "\x8E\x40", 'Big5-HKSCS') # 𣻗 - #assert_raise(Encoding::UndefinedConversionError) { "\xF9\xD6".encode("utf-8", 'Big5-HKSCS') } + #assert_undefined_in("\xF9\xD6", 'Big5-HKSCS') check_both_ways("\u795E\u6797\u7FA9\u535A", "\xAF\xAB\xAA\x4C\xB8\x71\xB3\xD5", 'Big5-HKSCS') # ç¥žæž—ç¾©åš end @@ -2275,7 +2286,7 @@ class TestTranscode < Test::Unit::TestCase result = th.map(&:value) end end - expected = "\xa4\xa2".force_encoding(Encoding::EUC_JP) + expected = "\xa4\xa2".dup.force_encoding(Encoding::EUC_JP) assert_equal([expected]*num, result, bug11277) end; end @@ -2308,4 +2319,121 @@ class TestTranscode < Test::Unit::TestCase assert_equal("A\nB\nC", s.encode(usascii, lf_newline: true)) 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) + assert_conversion_both_ways(utf8, 'utf-8', raw, encoding) + end + alias check_both_ways assert_conversion_both_ways_utf8 + + def assert_conversion_both_ways(str1, enc1, str2, enc2) + message = str1.dump+str2.dump + assert_equal(str1.force_encoding(enc1), str2.encode(enc1, enc2), message) + assert_equal(str2.force_encoding(enc2), str1.encode(enc2, enc1), message) + end + alias check_both_ways2 assert_conversion_both_ways + + def assert_undefined_conversion(str, to, from = nil) + assert_raise(Encoding::UndefinedConversionError) { str.encode(to, from) } + end + + def assert_undefined_in(str, encoding) + assert_undefined_conversion(str, 'utf-8', encoding) + end + + def assert_invalid_byte_sequence(str, to, from = nil) + assert_raise(Encoding::InvalidByteSequenceError) { str.encode(to, from) } + end + + def assert_invalid_in(str, encoding) + assert_invalid_byte_sequence(str, 'utf-8', encoding) + end end diff --git a/test/ruby/test_variable.rb b/test/ruby/test_variable.rb index 1158313776..13b8a7905f 100644 --- a/test/ruby/test_variable.rb +++ b/test/ruby/test_variable.rb @@ -49,6 +49,19 @@ class TestVariable < Test::Unit::TestCase assert_equal(1, c.class_variable_get(:@@foo)) end + Zeus = Gods.clone + + def test_cloned_allows_setting_cvar + Zeus.class_variable_set(:@@rule, "Athena") + + god = Gods.new.ruler0 + zeus = Zeus.new.ruler0 + + assert_equal "Cronus", god + assert_equal "Athena", zeus + assert_not_equal god.object_id, zeus.object_id + end + def test_singleton_class_included_class_variable c = Class.new c.extend(Olympians) @@ -375,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 @@ -394,12 +462,89 @@ 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) assert_equal(%i(v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11), v, bug11674) end + def test_many_instance_variables + objects = [Object.new, Hash.new, Module.new] + objects.each do |obj| + 1000.times do |i| + obj.instance_variable_set("@var#{i}", i) + end + 1000.times do |i| + assert_equal(i, obj.instance_variable_get("@var#{i}")) + end + end + end + + def test_local_variables_encoding + α = 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 9c06ec14fb..d183e03391 100644 --- a/test/ruby/test_vm_dump.rb +++ b/test/ruby/test_vm_dump.rb @@ -1,21 +1,23 @@ # frozen_string_literal: true require 'test/unit' +return unless /darwin/ =~ RUBY_PLATFORM + class TestVMDump < Test::Unit::TestCase - def assert_darwin_vm_dump_works(args) - omit if RUBY_PLATFORM !~ /darwin/ - assert_in_out_err(args, "", [], /^\[IMPORTANT\]/) + def assert_darwin_vm_dump_works(args, timeout=nil) + args.unshift({"RUBY_ON_BUG" => nil, "RUBY_CRASH_REPORT" => nil}) + assert_in_out_err(args, "", [], /^\[IMPORTANT\]/, timeout: timeout || 300) end def test_darwin_invalid_call - assert_darwin_vm_dump_works(['-rfiddle', '-eFiddle::Function.new(Fiddle::Pointer.new(1), [], Fiddle::TYPE_VOID).call']) + assert_darwin_vm_dump_works(['-r-test-/fatal', '-eBug.invalid_call(1)']) 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 - assert_darwin_vm_dump_works(['-rfiddle', '-eFiddle.dlunwrap(100).inspect']) + assert_darwin_vm_dump_works(['-r-test-/fatal', '-eBug.invalid_access(100)']) end end diff --git a/test/ruby/test_warning.rb b/test/ruby/test_warning.rb new file mode 100644 index 0000000000..cd220ff00f --- /dev/null +++ b/test/ruby/test_warning.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'test/unit' + +class TestWarning < Test::Unit::TestCase + def test_warn_called_only_when_category_enabled + # Assert that warn is called when the category is enabled + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + Warning[:deprecated] = true + $warnings = [] + def Warning.warn(msg, category:) + $warnings << [msg, category] + end + assert_equal(0, $warnings.length) + "" << 12 + assert_equal(1, $warnings.length) + end; + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + Warning[:deprecated] = false + $warnings = [] + def Warning.warn(msg, category:) + $warnings << [msg, category] + end + assert_equal(0, $warnings.length) + "" << 12 + assert_equal(0, $warnings.length, $warnings.join) + end; + end +end diff --git a/test/ruby/weakkeymap.rb b/test/ruby/test_weakkeymap.rb index 20bfe5ec2c..91c1538076 100644 --- a/test/ruby/weakkeymap.rb +++ b/test/ruby/test_weakkeymap.rb @@ -33,11 +33,24 @@ class TestWeakKeyMap < Test::Unit::TestCase end def test_key? - 1.times do - assert_weak_include(:key?, "foo") + assert_weak_include(:key?, "foo") + assert_not_send([@wm, :key?, "bar"]) + end + + def test_delete + k1 = "foo" + x1 = Object.new + @wm[k1] = x1 + assert_equal x1, @wm[k1] + assert_equal x1, @wm.delete(k1) + assert_nil @wm[k1] + assert_nil @wm.delete(k1) + + fallback = @wm.delete(k1) do |key| + assert_equal k1, key + 42 end - GC.start - assert_not_send([@wm, :key?, "FOO".downcase]) + assert_equal 42, fallback end def test_clear @@ -48,6 +61,20 @@ class TestWeakKeyMap < Test::Unit::TestCase refute @wm[k] end + def test_clear_bug_20691 + assert_normal_exit(<<~RUBY) + map = ObjectSpace::WeakKeyMap.new + + 1_000.times do + 1_000.times do + map[Object.new] = nil + end + + map.clear + end + RUBY + end + def test_inspect x = Object.new k = Object.new @@ -74,7 +101,7 @@ class TestWeakKeyMap < Test::Unit::TestCase assert_nothing_raised(FrozenError) {@wm['foo'] = o} end - def test_inconsistent_hash_key + def test_inconsistent_hash_key_memory_leak assert_no_memory_leak [], '', <<~RUBY class BadHash def initialize @@ -95,6 +122,26 @@ class TestWeakKeyMap < Test::Unit::TestCase RUBY end + def test_compaction + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + assert_separately(%w(-robjspace), <<-'end;') + wm = ObjectSpace::WeakKeyMap.new + key = Object.new + val = Object.new + wm[key] = val + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_equal(val, wm[key]) + end; + end + + def test_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress { ObjectSpace::WeakKeyMap.new } + end + private def assert_weak_include(m, k, n = 100) diff --git a/test/ruby/test_weakmap.rb b/test/ruby/test_weakmap.rb index c72e7310db..4f5823ecf4 100644 --- a/test/ruby/test_weakmap.rb +++ b/test/ruby/test_weakmap.rb @@ -78,10 +78,26 @@ class TestWeakMap < Test::Unit::TestCase @wm[i] = Object.new @wm.inspect end - assert_match(/\A\#<#{@wm.class.name}:[^:]++:(?:\s\d+\s=>\s\#<(?:Object|collected):[^:<>]*+>(?:,|>\z))+/, + assert_match(/\A\#<#{@wm.class.name}:0x[\da-f]+(?::(?: \d+ => \#<(?:Object|collected):0x[\da-f]+>,?)+)?>\z/, @wm.inspect) end + def test_delete + k1 = "foo" + x1 = Object.new + @wm[k1] = x1 + assert_equal x1, @wm[k1] + assert_equal x1, @wm.delete(k1) + assert_nil @wm[k1] + assert_nil @wm.delete(k1) + + fallback = @wm.delete(k1) do |key| + assert_equal k1, key + 42 + end + assert_equal 42, fallback + end + def test_each m = __callee__[/test_(.*)/, 1] x1 = Object.new @@ -177,14 +193,17 @@ class TestWeakMap < Test::Unit::TestCase end; end - def test_compaction_bug_19529 + def test_compaction + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + # [Bug #19529] obj = Object.new 100.times do |i| GC.compact @wm[i] = obj end - assert_separately(%w(--disable-gems), <<-'end;') + assert_ruby_status([], <<-'end;') wm = ObjectSpace::WeakMap.new obj = Object.new 100.times do @@ -193,6 +212,34 @@ class TestWeakMap < Test::Unit::TestCase end GC.compact end; + + assert_separately(%w(-robjspace), <<-'end;') + wm = ObjectSpace::WeakMap.new + key = Object.new + val = Object.new + wm[key] = val + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_equal(val, wm[key]) + end; + + assert_ruby_status(["-W0"], <<-'end;') + wm = ObjectSpace::WeakMap.new + + ary = 10_000.times.map do + o = Object.new + wm[o] = 1 + o + end + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + end; + end + + def test_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress { ObjectSpace::WeakMap.new } end def test_replaced_values_bug_19531 @@ -211,4 +258,34 @@ class TestWeakMap < Test::Unit::TestCase assert_equal b, @wm[1] end + + def test_use_after_free_bug_20688 + assert_normal_exit(<<~RUBY) + weakmap = ObjectSpace::WeakMap.new + 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_whileuntil.rb b/test/ruby/test_whileuntil.rb index 121c44817d..ff6d29ac4a 100644 --- a/test/ruby/test_whileuntil.rb +++ b/test/ruby/test_whileuntil.rb @@ -73,6 +73,24 @@ class TestWhileuntil < Test::Unit::TestCase } end + def test_begin_while + i = 0 + sum = 0 + begin + i += 1 + sum += i + end while i < 10 + assert_equal([10, 55], [i, sum]) + + i = 0 + sum = 0 + ( + i += 1 + sum += i + ) while false + assert_equal([0, 0], [i, sum]) + end + def test_until i = 0 until i>4 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 3d354bfc6c..d6b9b75648 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -10,6 +10,8 @@ require_relative '../lib/jit_support' return unless JITSupport.yjit_supported? +require 'stringio' + # Tests for YJIT with assertions on compilation and side exits # insipired by the RJIT tests in test/ruby/test_rjit.rb class TestYJIT < Test::Unit::TestCase @@ -51,8 +53,127 @@ class TestYJIT < Test::Unit::TestCase #assert_in_out_err('--yjit-call-threshold=', '', [], /--yjit-call-threshold needs an argument/) end + def test_yjit_enable + args = [] + args << "--disable=yjit" if RubyVM::YJIT.enabled? + assert_separately(args, <<~'RUBY') + refute_predicate RubyVM::YJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+YJIT" + + RubyVM::YJIT.enable + + assert_predicate RubyVM::YJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+YJIT" + RUBY + end + + def test_yjit_disable + assert_separately(["--yjit", "--yjit-disable"], <<~'RUBY') + refute_predicate RubyVM::YJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+YJIT" + + RubyVM::YJIT.enable + + assert_predicate RubyVM::YJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+YJIT" + RUBY + end + + def test_yjit_enable_stats_false + assert_separately(["--yjit-disable", "--yjit-stats"], <<~RUBY, ignore_stderr: true) + assert_false RubyVM::YJIT.enabled? + assert_nil RubyVM::YJIT.runtime_stats + + RubyVM::YJIT.enable + + assert_true RubyVM::YJIT.enabled? + assert_true RubyVM::YJIT.runtime_stats[:all_stats] + RUBY + end + + def test_yjit_enable_stats_true + args = [] + args << "--disable=yjit" if RubyVM::YJIT.enabled? + assert_separately(args, <<~RUBY, ignore_stderr: true) + assert_false RubyVM::YJIT.enabled? + assert_nil RubyVM::YJIT.runtime_stats + + RubyVM::YJIT.enable(stats: true) + + assert_true RubyVM::YJIT.enabled? + assert_true RubyVM::YJIT.runtime_stats[:all_stats] + RUBY + end + + def test_yjit_enable_stats_quiet + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(stats: true)']) do |_stdout, stderr, _status| + assert_not_empty stderr + end + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(stats: :quiet)']) do |_stdout, stderr, _status| + assert_empty stderr + end + end + + def test_yjit_enable_with_call_threshold + assert_separately(%w[--yjit-disable --yjit-call-threshold=1], <<~RUBY) + def not_compiled = nil + def will_compile = nil + def compiled_counts = RubyVM::YJIT.runtime_stats&.dig(:compiled_iseq_count) + + not_compiled + assert_nil compiled_counts + assert_false RubyVM::YJIT.enabled? + + RubyVM::YJIT.enable + + will_compile + assert compiled_counts > 0 + assert_true RubyVM::YJIT.enabled? + RUBY + end + + def test_yjit_enable_with_monkey_patch + assert_ruby_status(%w[--yjit-disable], <<~RUBY) + # This lets rb_method_entry_at(rb_mKernel, ...) return NULL + Kernel.prepend(Module.new) + + # This must not crash with "undefined optimized method!" + RubyVM::YJIT.enable + RUBY + end + + def test_yjit_enable_with_valid_runtime_call_threshold_option + assert_in_out_err(['--yjit-disable', '-e', + 'RubyVM::YJIT.enable(call_threshold: 1); puts RubyVM::YJIT.enabled?']) do |stdout, stderr, _status| + assert_empty stderr + assert_include stdout.join, "true" + end + end + + def test_yjit_enable_with_invalid_runtime_call_threshold_option + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(mem_size: 0)']) do |stdout, stderr, status| + assert_not_empty stderr + assert_match(/ArgumentError/, stderr.join) + assert_equal 1, status.exitstatus + end + end + + def test_yjit_enable_with_invalid_runtime_mem_size_option + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(mem_size: 0)']) do |stdout, stderr, status| + assert_not_empty stderr + assert_match(/ArgumentError/, stderr.join) + assert_equal 1, status.exitstatus + 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 = EnvUtil.invoke_ruby(%w(-v --yjit-stats), '', true, true) + _stdout, stderr, _status = invoke_ruby(%w(-v --yjit-stats), '', true, true) refute_includes(stderr, "NoMethodError") end @@ -241,10 +362,10 @@ class TestYJIT < Test::Unit::TestCase end def test_compile_opt_aset - assert_compiles('[1,2,3][2] = 4', insns: %i[opt_aset]) - assert_compiles('{}[:foo] = :bar', insns: %i[opt_aset]) - assert_compiles('[1,2,3][0..-1] = []', insns: %i[opt_aset]) - assert_compiles('"foo"[3] = "d"', insns: %i[opt_aset]) + assert_compiles('[1,2,3][2] = 4', insns: %i[opt_aset], frozen_string_literal: false) + assert_compiles('{}[:foo] = :bar', insns: %i[opt_aset], frozen_string_literal: false) + assert_compiles('[1,2,3][0..-1] = []', insns: %i[opt_aset], frozen_string_literal: false) + assert_compiles('"foo"[3] = "d"', insns: %i[opt_aset], frozen_string_literal: false) end def test_compile_attr_set @@ -453,6 +574,32 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_opt_getconstant_path_general + assert_compiles(<<~RUBY, result: [1, 1]) + module Base + Const = 1 + end + + class Sub + def const + _const = nil # make a non-entry block for opt_getconstant_path + Const + end + + def self.const_missing(n) + Base.const_get(n) + end + end + + + sub = Sub.new + result = [] + result << sub.const # generate the general case + result << sub.const # const_missing does not invalidate the block + result + RUBY + end + def test_string_interpolation assert_compiles(<<~'RUBY', insns: %i[objtostring anytostring concatstrings], result: "foobar", call_threshold: 2) def make_str(foo, bar) @@ -510,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 @@ -524,8 +691,7 @@ class TestYJIT < Test::Unit::TestCase end def test_getblockparamproxy - # Currently two side exits as OPTIMIZED_METHOD_TYPE_CALL is unimplemented - assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: { opt_send_without_block: 2 }) + assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: {}) def foo &blk p blk.call p blk.call @@ -536,6 +702,24 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_ifunc_getblockparamproxy + assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: {}) + class Foo + include Enumerable + + def each(&block) + block.call 1 + block.call 2 + block.call 3 + end + end + + foo = Foo.new + foo.map { _1 * 2 } + foo.map { _1 * 2 } + RUBY + end + def test_send_blockarg assert_compiles(<<~'RUBY', insns: [:getblockparamproxy, :send], exits: {}) def bar @@ -584,7 +768,7 @@ class TestYJIT < Test::Unit::TestCase def test_send_kwargs # For now, this side-exits when calls include keyword args - assert_compiles(<<~'RUBY', result: "2#a:1,b:2/A", exits: {opt_send_without_block: 1}) + assert_compiles(<<~'RUBY', result: "2#a:1,b:2/A") def internal_method(**kw) "#{kw.size}##{kw.keys.map { |k| "#{k}:#{kw[k]}" }.join(",")}" end @@ -624,7 +808,7 @@ class TestYJIT < Test::Unit::TestCase def test_send_kwargs_splat # For now, this side-exits when calling with a splat - assert_compiles(<<~'RUBY', result: "2#a:1,b:2/B", exits: {opt_send_without_block: 1}) + assert_compiles(<<~'RUBY', result: "2#a:1,b:2/B") def internal_method(**kw) "#{kw.size}##{kw.keys.map { |k| "#{k}:#{kw[k]}" }.join(",")}" end @@ -638,7 +822,7 @@ class TestYJIT < Test::Unit::TestCase def test_send_block # Setlocal_wc_0 sometimes side-exits on write barrier - assert_compiles(<<~'RUBY', result: "b:n/b:y/b:y/b:n", exits: { :setlocal_WC_0 => 0..1 }) + assert_compiles(<<~'RUBY', result: "b:n/b:y/b:y/b:n") def internal_method(&b) "b:#{block_given? ? "y" : "n"}" end @@ -754,6 +938,25 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_super_with_alias + assert_compiles(<<~'RUBY', insns: %i[invokesuper opt_plus opt_mult], result: 15) + class A + def foo = 1 + 2 + end + + module M + def foo = super() * 5 + alias bar foo + + def foo = :bad + end + + A.prepend M + + A.new.bar + RUBY + end + def test_super_cfunc assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: "Hello") class Gnirts < String @@ -952,8 +1155,57 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_disable_code_gc_with_many_iseqs + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok, mem_size: 1, code_gc: false) + fiber = Fiber.new { + # Loop to call the same basic block again after Fiber.yield + while true + Fiber.yield(nil.to_i) + end + } + + return :not_paged1 unless add_pages(250) # use some pages + return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well + + add_pages(2000) # use a whole lot of pages to run out of 1MiB + return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable + + code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count] + return :"code_gc_#{code_gc_count}" if code_gc_count != 0 + + :ok + RUBY + end + def test_code_gc_with_many_iseqs - assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok, mem_size: 1) + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok, mem_size: 1, code_gc: true) + fiber = Fiber.new { + # Loop to call the same basic block again after Fiber.yield + while true + Fiber.yield(nil.to_i) + end + } + + return :not_paged1 unless add_pages(250) # use some pages + return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well + + add_pages(2000) # use a whole lot of pages to run out of 1MiB + return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable + + code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count] + return :"code_gc_#{code_gc_count}" if code_gc_count == 0 + + :ok + RUBY + end + + def test_code_gc_with_auto_compact + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + assert_compiles((code_gc_helpers + <<~'RUBY'), exits: :any, result: :ok, mem_size: 1, code_gc: true) + # Test ISEQ moves in the middle of code GC + GC.auto_compact = true + fiber = Fiber.new { # Loop to call the same basic block again after Fiber.yield while true @@ -1064,7 +1316,7 @@ class TestYJIT < Test::Unit::TestCase def test_bug_19316 n = 2 ** 64 # foo's extra param and the splats are relevant - assert_compiles(<<~'RUBY', result: [[n, -n], [n, -n]]) + assert_compiles(<<~'RUBY', result: [[n, -n], [n, -n]], exits: :any) def foo(_, a, b, c) [a & b, ~c] end @@ -1078,6 +1330,8 @@ class TestYJIT < Test::Unit::TestCase end def test_gc_compact_cyclic_branch + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + assert_compiles(<<~'RUBY', result: 2) def foo i = 0 @@ -1094,7 +1348,7 @@ class TestYJIT < Test::Unit::TestCase end def test_invalidate_cyclic_branch - assert_compiles(<<~'RUBY', result: 2) + assert_compiles(<<~'RUBY', result: 2, exits: { opt_plus: 1 }) def foo i = 0 while i < 2 @@ -1112,7 +1366,7 @@ class TestYJIT < Test::Unit::TestCase end def test_tracing_str_uplus - assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok) + assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: { putspecialobject: 1 }) def str_uplus _ = 1 _ = 2 @@ -1157,7 +1411,7 @@ class TestYJIT < Test::Unit::TestCase def test_return_to_invalidated_block # [Bug #19463] - assert_compiles(<<~RUBY, result: [1, 1, :ugokanai]) + assert_compiles(<<~RUBY, result: [1, 1, :ugokanai], exits: { definesmethod: 1, getlocal_WC_0: 1 }) klass = Class.new do def self.lookup(hash, key) = hash[key] @@ -1196,9 +1450,62 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_return_to_invalidated_frame + assert_compiles(code_gc_helpers + <<~RUBY, exits: :any, result: :ok) + def jump + [] # something not inlined + end + + def entry(code_gc) + jit_exception(code_gc) + jump # faulty jump after code GC. #jit_exception should not come back. + end + + def jit_exception(code_gc) + if code_gc + tap do + RubyVM::YJIT.code_gc + break # jit_exec_exception catches TAG_BREAK and re-enters JIT code + end + end + end + + add_pages(100) + jump # Compile #jump in a non-first page + add_pages(100) + entry(false) # Compile #entry and its call to #jump in another page + entry(true) # Free #jump but not #entry + + :ok + RUBY + end + + def test_setivar_on_class + # Bug in https://github.com/ruby/ruby/pull/8152 + assert_compiles(<<~RUBY, result: :ok) + class Base + def self.or_equal + @or_equal ||= Object.new + end + end + + Base.or_equal # ensure compiled + + class Child < Base + end + + 200.times do |iv| # Need to be more than MAX_IVAR + Child.instance_variable_set("@_iv_\#{iv}", Object.new) + end + + Child.or_equal + :ok + RUBY + end + def test_nested_send #[Bug #19464] - assert_compiles(<<~RUBY, result: [:ok, :ok]) + assert_compiles(<<~RUBY, result: [:ok, :ok], exits: { defineclass: 1 }) klass = Class.new do class << self alias_method :my_send, :send @@ -1216,6 +1523,299 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_str_concat_encoding_mismatch + assert_compiles(<<~'RUBY', result: "incompatible character encodings: BINARY (ASCII-8BIT) and EUC-JP") + def bar(a, b) + a << b + rescue => e + e.message + end + + def foo(a, b, h) + h[nil] + bar(a, b) # Ruby call, not set cfp->pc + end + + h = Hash.new { nil } + foo("\x80".b, "\xA1A1".dup.force_encoding("EUC-JP"), h) + foo("\x80".b, "\xA1A1".dup.force_encoding("EUC-JP"), h) + RUBY + end + + def test_io_reopen_clobbering_singleton_class + assert_compiles(<<~RUBY, result: [:ok, :ok], exits: { definesmethod: 1, opt_eq: 2 }) + def $stderr.to_i = :i + + def test = $stderr.to_i + + [test, test] + $stderr.reopen($stderr.dup) + [test, test].map { :ok unless _1 == :i } + RUBY + end + + def test_proc_block_arg + assert_compiles(<<~RUBY, result: [:proc, :no_block]) + def yield_if_given = block_given? ? yield : :no_block + + def call(block_arg = nil) = yield_if_given(&block_arg) + + [call(-> { :proc }), call] + RUBY + end + + def test_opt_mult_overflow + assert_no_exits('0xfff_ffff_ffff_ffff * 0x10') + end + + def test_disable_stats + assert_in_out_err(%w[--yjit-stats --yjit-disable]) + end + + def test_odd_calls_to_attr_reader + # Use of delegate from ActiveSupport use these kind of calls to getter methods. + assert_compiles(<<~RUBY, result: [1, 1, 1], no_send_fallbacks: true) + class One + attr_reader :one + def initialize + @one = 1 + end + end + + def calls(obj, empty, &) + [obj.one(*empty), obj.one(&), obj.one(*empty, &)] + end + + calls(One.new, []) + RUBY + end + + def test_kwrest + assert_compiles(<<~RUBY, result: true, no_send_fallbacks: true) + def req_rest(r1:, **kwrest) = [r1, kwrest] + def opt_rest(r1: 1.succ, **kwrest) = [r1, kwrest] + def kwrest(**kwrest) = kwrest + + def calls + [ + [1, {}] == req_rest(r1: 1), + [1, {:r2=>2, :r3=>3}] == req_rest(r1: 1, r2: 2, r3: 3), + [1, {:r2=>2, :r3=>3}] == req_rest(r2: 2, r1:1, r3: 3), + [1, {:r2=>2, :r3=>3}] == req_rest(r2: 2, r3: 3, r1: 1), + + [2, {}] == opt_rest, + [2, { r2: 2, r3: 3 }] == opt_rest(r2: 2, r3: 3), + [0, { r2: 2, r3: 3 }] == opt_rest(r1: 0, r3: 3, r2: 2), + [0, { r2: 2, r3: 3 }] == opt_rest(r2: 2, r1: 0, r3: 3), + [1, { r2: 2, r3: 3 }] == opt_rest(r2: 2, r3: 3, r1: 1), + + {} == kwrest, + { r0: 88, r1: 99 } == kwrest(r0: 88, r1: 99), + ] + end + + calls.all? + RUBY + end + + def test_send_polymorphic_method_name + assert_compiles(<<~'RUBY', result: %i[ok ok], no_send_fallbacks: true) + mid = "dynamic_mid_#{rand(100..200)}" + mid_dsym = mid.to_sym + + define_method(mid) { :ok } + + define_method(:send_site) { send(_1) } + + [send_site(mid), send_site(mid_dsym)] + RUBY + end + + def test_kw_splat_nil + assert_compiles(<<~'RUBY', result: %i[ok ok], no_send_fallbacks: true) + def id(x) = x + def kw_fw(arg, **) = id(arg, **) + def use = [kw_fw(:ok), :ok.itself(**nil)] + + use + RUBY + end + + def test_empty_splat + assert_compiles(<<~'RUBY', result: :ok, no_send_fallbacks: true) + def foo = :ok + def use(empty) = foo(*empty) + + use([]) + RUBY + end + + def test_byteslice_sp_invalidation + assert_compiles(<<~'RUBY', result: 'ok', no_send_fallbacks: true) + "okng".itself.byteslice(0, 2) + RUBY + end + + def test_leaf_builtin + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: 1) + before = RubyVM::YJIT.runtime_stats[:num_send_iseq_leaf] + return 1 if before.nil? + + def entry = self.class + entry + + after = RubyVM::YJIT.runtime_stats[:num_send_iseq_leaf] + after - before + RUBY + end + + def test_runtime_stats_types + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + stats = RubyVM::YJIT.runtime_stats + return true unless stats[:all_stats] + + [ + stats[:object_shape_count].is_a?(Integer), + stats[:ratio_in_yjit].nil? || stats[:ratio_in_yjit].is_a?(Float), + ].all? + RUBY + end + + def test_runtime_stats_key_arg + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + # Collect single stat. + stat = RubyVM::YJIT.runtime_stats(:yjit_alloc_size) + + # Ensure this invocation had stats. + return true unless RubyVM::YJIT.runtime_stats[:all_stats] + + stat > 0.0 + RUBY + end + + def test_runtime_stats_arg_error + assert_compiles(<<~'RUBY', exits: :any, result: true) + begin + RubyVM::YJIT.runtime_stats(Object.new) + :no_error + rescue TypeError => e + e.message == "non-symbol given" + end + RUBY + end + + def test_runtime_stats_unknown_key + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + RubyVM::YJIT.runtime_stats(:some_key_unlikely_to_exist).nil? + RUBY + end + + def test_yjit_option_uses_array_each_in_ruby + assert_separately(["--yjit"], <<~'RUBY') + # Array#each should be implemented in Ruby for YJIT + assert_equal "<internal:array>", Array.instance_method(:each).source_location.first + + # The backtrace, however, should not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:11:in 'Array#each'", e.backtrace[1] + end + RUBY + end + + def test_yjit_enable_replaces_array_each + assert_separately([*("--disable=yjit" if RubyVM::YJIT.enabled?)], <<~'RUBY') + # Array#each should be implemented in C for the interpreter + assert_nil Array.instance_method(:each).source_location + + # The backtrace should not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:11:in 'Array#each'", e.backtrace[1] + end + + RubyVM::YJIT.enable + + # Array#each should be implemented in Ruby for YJIT + assert_equal "<internal:array>", Array.instance_method(:each).source_location.first + + # However, the backtrace should still not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:23:in 'Array#each'", e.backtrace[1] + end + RUBY + end + + def test_yjit_enable_preserves_array_each_monkey_patch + assert_separately([*("--disable=yjit" if RubyVM::YJIT.enabled?)], <<~'RUBY') + # Array#each should be implemented in C initially + assert_nil Array.instance_method(:each).source_location + + # Override Array#each + $called = false + Array.prepend(Module.new { + def each + $called = true + super + end + }) + + RubyVM::YJIT.enable + + # The monkey-patch should still be alive + [].each {} + assert_true $called + + # YJIT should not replace Array#each with the "<internal:array>" one + assert_equal "-", Array.instance_method(:each).source_location.first + RUBY + end + + def test_yield_kwargs + assert_compiles(<<~RUBY, result: 3, no_send_fallbacks: true) + def req2kws = yield a: 1, b: 2 + + req2kws { |a:, b:| a + b } + 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 @@ -1227,9 +1827,9 @@ class TestYJIT < Test::Unit::TestCase end def add_pages(num_jits) - pages = RubyVM::YJIT.runtime_stats[:compiled_page_count] + pages = RubyVM::YJIT.runtime_stats[:live_page_count] num_jits.times { return false unless eval('compiles { nil.to_i }') } - pages.nil? || pages < RubyVM::YJIT.runtime_stats[:compiled_page_count] + pages.nil? || pages < RubyVM::YJIT.runtime_stats[:live_page_count] end RUBY end @@ -1239,7 +1839,17 @@ class TestYJIT < Test::Unit::TestCase end ANY = Object.new - def assert_compiles(test_script, insns: [], call_threshold: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil, mem_size: nil) + def assert_compiles( + test_script, insns: [], + call_threshold: 1, + stdout: nil, + exits: {}, + result: ANY, + frozen_string_literal: nil, + mem_size: nil, + code_gc: false, + no_send_fallbacks: false + ) reset_stats = <<~RUBY RubyVM::YJIT.runtime_stats RubyVM::YJIT.reset_stats! @@ -1264,7 +1874,7 @@ class TestYJIT < Test::Unit::TestCase RUBY script = <<~RUBY - #{"# frozen_string_literal: true" if frozen_string_literal} + #{"# frozen_string_literal: " + frozen_string_literal.to_s unless frozen_string_literal.nil?} _test_proc = -> { #{test_script} } @@ -1273,7 +1883,7 @@ class TestYJIT < Test::Unit::TestCase #{write_results} RUBY - status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:) + status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:, code_gc:) assert status.success?, "exited with status #{status.to_i}, stderr:\n#{err}" @@ -1299,12 +1909,23 @@ class TestYJIT < Test::Unit::TestCase # barriers, cache misses.) if exits != :any && exits != recorded_exits && - !exits.all? { |k, v| v === recorded_exits[k] } # triple-equal checks range membership or integer equality - flunk "Expected #{exits.empty? ? "no" : exits.inspect} exits" \ - ", but got\n#{recorded_exits.inspect}" + (exits.keys != recorded_exits.keys || !exits.all? { |k, v| v === recorded_exits[k] }) # triple-equal checks range membership or integer equality + stats_reasons = StringIO.new + ::RubyVM::YJIT.send(:_print_stats_reasons, runtime_stats, stats_reasons) + stats_reasons = stats_reasons.string + flunk <<~EOM + Expected #{exits.empty? ? "no" : exits.inspect} exits, but got: + #{recorded_exits.inspect} + Reasons: + #{stats_reasons} + EOM end end + if no_send_fallbacks + assert_equal(0, runtime_stats[:num_send_dynamic], "Expected no use of fallback implementation") + end + # Only available when --enable-yjit=dev if runtime_stats[:all_stats] missed_insns = insns.dup @@ -1327,22 +1948,38 @@ class TestYJIT < Test::Unit::TestCase s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join end - def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil) + def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil, code_gc: false) args = [ "--disable-gems", "--yjit-call-threshold=#{call_threshold}", - "--yjit-stats" + "--yjit-stats=quiet" ] args << "--yjit-exec-mem-size=#{mem_size}" if mem_size + args << "--yjit-code-gc" if code_gc args << "-e" << script_shell_encode(script) stats_r, stats_w = IO.pipe - out, err, status = EnvUtil.invoke_ruby(args, - '', true, true, timeout: timeout, ios: {3 => stats_w} - ) + # Separate thread so we don't deadlock when + # the child ruby blocks writing the stats to fd 3 + stats = '' + stats_reader = Thread.new do + stats = stats_r.read + stats_r.close + end + out, err, status = invoke_ruby(args, '', true, true, timeout: timeout, ios: { 3 => stats_w }) stats_w.close - stats = stats_r.read + stats_reader.join(timeout) stats = Marshal.load(stats) if !stats.empty? - stats_r.close [status, out, err, stats] + ensure + stats_reader&.kill + stats_reader&.join(timeout) + stats_r&.close + stats_w&.close + end + + # A wrapper of EnvUtil.invoke_ruby that uses RbConfig.ruby instead of EnvUtil.ruby + # that might use a wrong Ruby depending on your environment. + def invoke_ruby(*args, **kwargs) + EnvUtil.invoke_ruby(*args, rubybin: RbConfig.ruby, **kwargs) end end diff --git a/test/ruby/test_yjit_exit_locations.rb b/test/ruby/test_yjit_exit_locations.rb index 851a582470..816ab457ce 100644 --- a/test/ruby/test_yjit_exit_locations.rb +++ b/test/ruby/test_yjit_exit_locations.rb @@ -18,22 +18,8 @@ class TestYJITExitLocations < Test::Unit::TestCase refute_includes(stderr, "NoMethodError") end - def test_trace_exits_setclassvariable - script = 'class Foo; def self.foo; @@foo = 1; end; end; Foo.foo' - assert_exit_locations(script) - end - - def test_trace_exits_putobject - assert_exit_locations('true') - assert_exit_locations('123') - assert_exit_locations(':foo') - end - - def test_trace_exits_opt_not - assert_exit_locations('!false') - assert_exit_locations('!nil') - assert_exit_locations('!true') - assert_exit_locations('![]') + def test_trace_exits_expandarray_splat + assert_exit_locations('*arr = []') end private diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb new file mode 100644 index 0000000000..ad2df806d5 --- /dev/null +++ b/test/ruby/test_zjit.rb @@ -0,0 +1,4542 @@ +# frozen_string_literal: true +# +# This set of tests can be run with: +# make test-all TESTS=test/ruby/test_zjit.rb + +require 'test/unit' +require 'envutil' +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 + test + test + RUBY + end + + def test_nil + assert_compiles 'nil', %q{ + def test = nil + test + } + end + + def test_putobject + assert_compiles '1', %q{ + def test = 1 + test + } + end + + def test_putstring + assert_compiles '""', %q{ + def test = "#{""}" + test + }, insns: [:putstring] + end + + def test_putchilldedstring + assert_compiles '""', %q{ + def test = "" + test + }, insns: [:putchilledstring] + end + + def test_leave_param + assert_compiles '5', %q{ + def test(n) = n + test(5) + } + 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) + m = n + m + end + test(3) + } + 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 + def bar(a) = a - 1 + def baz(a, b) = a - b + + def test1 = foo + def test2 = bar(3) + def test3 = baz(4, 1) + + [test1, test2, test3] + } + 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 + test # profile opt_plus + test + }, call_threshold: 2 + end + + def test_opt_plus_fixnum + assert_compiles '3', %q{ + def test(a, b) = a + b + test(0, 1) # profile opt_plus + test(1, 2) + }, call_threshold: 2 + end + + def test_opt_plus_chain + assert_compiles '6', %q{ + def test(a, b, c) = a + b + c + test(0, 1, 2) # profile opt_plus + test(1, 2, 3) + }, call_threshold: 2 + end + + def test_opt_plus_left_imm + assert_compiles '3', %q{ + def test(a) = 1 + a + test(1) # profile opt_plus + test(2) + }, call_threshold: 2 + end + + def test_opt_plus_type_guard_exit + assert_compiles '[3, 3.0]', %q{ + def test(a) = 1 + a + test(1) # profile opt_plus + [test(2), test(2.0)] + }, call_threshold: 2 + end + + def test_opt_plus_type_guard_exit_with_locals + assert_compiles '[6, 6.0]', %q{ + def test(a) + local = 3 + 1 + a + local + end + test(1) # profile opt_plus + [test(2), test(2.0)] + }, call_threshold: 2 + end + + def test_opt_plus_type_guard_nested_exit + assert_compiles '[4, 4.0]', %q{ + def side_exit(n) = 1 + n + def jit_frame(n) = 1 + side_exit(n) + def entry(n) = jit_frame(n) + entry(2) # profile send + [entry(2), entry(2.0)] + }, call_threshold: 2 + end + + def test_opt_plus_type_guard_nested_exit_with_locals + assert_compiles '[9, 9.0]', %q{ + def side_exit(n) + local = 2 + 1 + n + local + end + def jit_frame(n) + local = 3 + 1 + side_exit(n) + local + end + def entry(n) = jit_frame(n) + entry(2) # profile send + [entry(2), entry(2.0)] + }, call_threshold: 2 + end + + # Test argument ordering + def test_opt_minus + assert_compiles '2', %q{ + def test(a, b) = a - b + test(2, 1) # profile opt_minus + test(6, 4) + }, call_threshold: 2 + end + + def test_opt_mult + assert_compiles '6', %q{ + def test(a, b) = a * b + test(1, 2) # profile opt_mult + test(2, 3) + }, call_threshold: 2 + end + + def test_opt_mult_overflow + assert_compiles '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{ + def test(a, b) + a * b + end + test(1, 1) # profile opt_mult + + r1 = test(2, 3) + r2 = test(2, -3) + r3 = test(2 << 40, 2 << 41) + r4 = test(2 << 40, -2 << 41) + r5 = test(1 << 62, 1 << 62) + + [r1, r2, r3, r4, r5] + }, call_threshold: 2 + end + + def test_opt_eq + assert_compiles '[true, false]', %q{ + def test(a, b) = a == b + test(0, 2) # profile opt_eq + [test(1, 1), test(0, 1)] + }, insns: [:opt_eq], call_threshold: 2 + end + + def test_opt_eq_with_minus_one + assert_compiles '[false, true]', %q{ + def test(a) = a == -1 + test(1) # profile opt_eq + [test(0), test(-1)] + }, insns: [:opt_eq], call_threshold: 2 + end + + def test_opt_neq_dynamic + # TODO(max): Don't split this test; instead, run all tests with and without + # profiling. + assert_compiles '[false, true]', %q{ + def test(a, b) = a != b + test(0, 2) # profile opt_neq + [test(1, 1), test(0, 1)] + }, insns: [:opt_neq], call_threshold: 1 + end + + def test_opt_neq_fixnum + assert_compiles '[false, true]', %q{ + def test(a, b) = a != b + test(0, 2) # profile opt_neq + [test(1, 1), test(0, 1)] + }, call_threshold: 2 + end + + def test_opt_lt + assert_compiles '[true, false, false]', %q{ + def test(a, b) = a < b + test(2, 3) # profile opt_lt + [test(0, 1), test(0, 0), test(1, 0)] + }, insns: [:opt_lt], call_threshold: 2 + end + + def test_opt_lt_with_literal_lhs + assert_compiles '[false, false, true]', %q{ + def test(n) = 2 < n + test(2) # profile opt_lt + [test(1), test(2), test(3)] + }, insns: [:opt_lt], call_threshold: 2 + end + + def test_opt_le + assert_compiles '[true, true, false]', %q{ + def test(a, b) = a <= b + test(2, 3) # profile opt_le + [test(0, 1), test(0, 0), test(1, 0)] + }, insns: [:opt_le], call_threshold: 2 + end + + def test_opt_gt + assert_compiles '[false, false, true]', %q{ + def test(a, b) = a > b + test(2, 3) # profile opt_gt + [test(0, 1), test(0, 0), test(1, 0)] + }, insns: [:opt_gt], call_threshold: 2 + end + + def test_opt_empty_p + assert_compiles('[false, false, true]', <<~RUBY, insns: [:opt_empty_p]) + def test(x) = x.empty? + return test([1]), test("1"), test({}) + RUBY + end + + def test_opt_succ + assert_compiles('[0, "B"]', <<~RUBY, insns: [:opt_succ]) + def test(obj) = obj.succ + return test(-1), test("A") + RUBY + end + + def test_opt_and + assert_compiles('[1, [3, 2, 1]]', <<~RUBY, insns: [:opt_and]) + def test(x, y) = x & y + return test(0b1101, 3), test([3, 2, 1, 4], [8, 1, 2, 3]) + RUBY + end + + def test_opt_or + assert_compiles('[11, [3, 2, 1]]', <<~RUBY, insns: [:opt_or]) + def test(x, y) = x | y + return test(0b1000, 3), test([3, 2, 1], [1, 2, 3]) + RUBY + end + + def test_fixnum_and + assert_compiles '[1, 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 + assert_compiles '[5]', %q{ + def a = 5 + def test = [a] + test + } + end + + def test_new_array_order + assert_compiles '[3, 2, 1]', %q{ + def a = 3 + def b = 2 + def c = 1 + def test = [a, b, c] + test + } + end + + def test_array_dup + assert_compiles '[1, 2, 3]', %q{ + def test = [1,2,3] + test + } + end + + def test_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) + if n < 5 + 0 + end + end + [test(3), test(7)] + } + end + + def test_if_else + assert_compiles '[0, 1]', %q{ + def test(n) + if n < 5 + 0 + else + 1 + end + end + [test(3), test(7)] + } + end + + def test_if_else_params + assert_compiles '[1, 20]', %q{ + def test(n, a, b) + if n < 5 + a + else + b + end + end + [test(3, 1, 2), test(7, 10, 20)] + } + end + + def test_if_else_nested + assert_compiles '[3, 8, 9, 14]', %q{ + def test(a, b, c, d, e) + if 2 < a + if a < 4 + b + else + c + end + else + if a < 0 + d + else + e + end + end + end + [ + test(-1, 1, 2, 3, 4), + test( 0, 5, 6, 7, 8), + test( 3, 9, 10, 11, 12), + test( 5, 13, 14, 15, 16), + ] + } + end + + def test_if_else_chained + assert_compiles '[12, 11, 21]', %q{ + def test(a) + (if 2 < a then 1 else 2 end) + (if a < 4 then 10 else 20 end) + end + [test(0), test(3), test(5)] + } + end + + def test_if_elsif_else + assert_compiles '[0, 2, 1]', %q{ + def test(n) + if n < 5 + 0 + elsif 8 < n + 1 + else + 2 + end + end + [test(3), test(7), test(9)] + } + end + + def test_ternary_operator + assert_compiles '[1, 20]', %q{ + def test(n, a, b) + n < 5 ? a : b + end + [test(3, 1, 2), test(7, 10, 20)] + } + end + + def test_ternary_operator_nested + assert_compiles '[2, 21]', %q{ + def test(n, a, b) + (n < 5 ? a : b) + 1 + end + [test(3, 1, 2), test(7, 10, 20)] + } + end + + def test_while_loop + assert_compiles '10', %q{ + def test(n) + i = 0 + while i < n + i = i + 1 + end + i + end + test(10) + } + end + + def test_while_loop_chain + assert_compiles '[135, 270]', %q{ + def test(n) + i = 0 + while i < n + i = i + 1 + end + while i < n * 10 + i = i * 3 + end + i + end + [test(5), test(10)] + } + end + + def test_while_loop_nested + assert_compiles '[0, 4, 12]', %q{ + def test(n, m) + i = 0 + while i < n + j = 0 + while j < m + j += 2 + end + i += j + end + i + end + [test(0, 0), test(1, 3), test(10, 5)] + } + end + + def test_while_loop_if_else + assert_compiles '[9, -1]', %q{ + def test(n) + i = 0 + while i < n + if n >= 10 + return -1 + else + i = i + 1 + end + end + i + end + [test(9), test(10)] + } + end + + def test_if_while_loop + assert_compiles '[9, 12]', %q{ + def test(n) + i = 0 + if n < 10 + while i < n + i += 1 + end + else + while i < n + i += 3 + end + end + i + end + [test(9), test(10)] + } + end + + def test_live_reg_past_ccall + assert_compiles '2', %q{ + def callee = 1 + def test = callee + callee + test + } + end + + def test_method_call + assert_compiles '12', %q{ + def callee(a, b) + a - b + end + + def test + callee(4, 2) + 10 + end + + test # profile test + test + }, call_threshold: 2 + end + + def test_recursive_fact + assert_compiles '[1, 6, 720]', %q{ + def fact(n) + if n == 0 + return 1 + end + return n * fact(n-1) + end + [fact(0), fact(3), fact(6)] + } + end + + def test_profiled_fact + assert_compiles '[1, 6, 720]', %q{ + def fact(n) + if n == 0 + return 1 + end + return n * fact(n-1) + end + fact(1) # profile fact + [fact(0), fact(3), fact(6)] + }, call_threshold: 3, num_profiles: 2 + end + + def test_recursive_fib + assert_compiles '[0, 2, 3]', %q{ + def fib(n) + if n < 2 + return n + end + return fib(n-1) + fib(n-2) + end + [fib(0), fib(3), fib(4)] + } + end + + def test_profiled_fib + assert_compiles '[0, 2, 3]', %q{ + def fib(n) + if n < 2 + return n + end + return fib(n-1) + fib(n-2) + end + fib(3) # profile fib + [fib(0), fib(3), fib(4)] + }, call_threshold: 5, num_profiles: 3 + end + + def test_spilled_basic_block_args + assert_compiles '55', %q{ + def test(n1, n2) + n3 = 3 + n4 = 4 + n5 = 5 + n6 = 6 + n7 = 7 + n8 = 8 + n9 = 9 + n10 = 10 + if n1 < n2 + n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8 + n9 + n10 + end + end + test(1, 2) + } + end + + def test_spilled_method_args + assert_runs '55', %q{ + def foo(n1, n2, n3, n4, n5, n6, n7, n8, n9, n10) + n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8 + n9 + n10 + end + + def test + foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + end + + test + } + + # TODO(Shopify/ruby#716): Support spills and change to assert_compiles + assert_runs '1', %q{ + def a(n1,n2,n3,n4,n5,n6,n7,n8,n9) = n1+n9 + a(2,0,0,0,0,0,0,0,-1) + } + + # TODO(Shopify/ruby#716): Support spills and change to assert_compiles + assert_runs '0', %q{ + def a(n1,n2,n3,n4,n5,n6,n7,n8) = n8 + a(1,1,1,1,1,1,1,0) + } + + # TODO(Shopify/ruby#716): Support spills and change to assert_compiles + # self param with spilled param + assert_runs '"main"', %q{ + def a(n1,n2,n3,n4,n5,n6,n7,n8) = self + a(1,0,0,0,0,0,0,0).to_s + } + end + + def test_spilled_param_new_arary + # TODO(Shopify/ruby#716): Support spills and change to assert_compiles + assert_runs '[:ok]', %q{ + def a(n1,n2,n3,n4,n5,n6,n7,n8) = [n8] + a(0,0,0,0,0,0,0, :ok) + } + end + + def test_forty_param_method + # This used to a trigger a miscomp on A64 due + # to a memory displacement larger than 9 bits. + # 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 + insn_names = RubyVM::INSTRUCTION_NAMES + zjit, others = insn_names.map.with_index.partition { |name, _| name.start_with?('zjit_') } + zjit_indexes = zjit.map(&:last) + other_indexes = others.map(&:last) + zjit_indexes.product(other_indexes).each do |zjit_index, other_index| + assert zjit_index > other_index, "'#{insn_names[zjit_index]}' at #{zjit_index} "\ + "must be defined after '#{insn_names[other_index]}' at #{other_index}" + 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, 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 + 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} + } + IO.open(#{pipe_fd}).write(Marshal.dump(result)) + RUBY + + out, err, status, result = eval_with_jit(script, pipe_fd:, **opts) + assert_success(out, err, status) + + result = Marshal.load(result) + assert_equal(expected, result.fetch(:ret_val).inspect) + + unless insns.empty? + iseq = result.fetch(:insns) + assert_equal( + "YARVInstructionSequence/SimpleDataFormat", + iseq.first, + "Failed to get ISEQ disassembly. " \ + "Make sure to put code directly under the '#{disasm_method}' method." + ) + iseq_insns = iseq.last + + 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, + 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) + 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 + 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) + # We can't pass utf-8-encoded characters directly in a shell arg. But we can use Ruby \u constants. + s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join + end +end |
