diff --git a/core/binding/fixtures/irb.rb b/core/binding/fixtures/irb.rb deleted file mode 100644 index 5f305f2d5d..0000000000 --- a/core/binding/fixtures/irb.rb +++ /dev/null @@ -1,3 +0,0 @@ -a = 10 - -binding.irb diff --git a/core/dir/chdir_spec.rb b/core/dir/chdir_spec.rb index 69a8b40415..015386a902 100644 --- a/core/dir/chdir_spec.rb +++ b/core/dir/chdir_spec.rb @@ -95,10 +95,10 @@ def to_str; DirSpecs.mock_dir; end end it "raises an Errno::ENOENT if the original directory no longer exists" do - dir1 = tmp('/testdir1') - dir2 = tmp('/testdir2') - File.should_not.exist?(dir1) - File.should_not.exist?(dir2) + dir1 = tmp('testdir1') + dir2 = tmp('testdir2') + Dir.should_not.exist?(dir1) + Dir.should_not.exist?(dir2) Dir.mkdir dir1 Dir.mkdir dir2 begin @@ -108,8 +108,8 @@ def to_str; DirSpecs.mock_dir; end end }.should raise_error(Errno::ENOENT) ensure - Dir.unlink dir1 if File.exist?(dir1) - Dir.unlink dir2 if File.exist?(dir2) + Dir.unlink dir1 if Dir.exist?(dir1) + Dir.unlink dir2 if Dir.exist?(dir2) end end @@ -177,28 +177,29 @@ def to_str; DirSpecs.mock_dir; end dir.close end - it "raises an Errno::ENOENT if the original directory no longer exists" do - dir_name1 = tmp('/testdir1') - dir_name2 = tmp('/testdir2') - File.should_not.exist?(dir_name1) - File.should_not.exist?(dir_name2) - Dir.mkdir dir_name1 - Dir.mkdir dir_name2 + platform_is_not :windows do + it "does not raise an Errno::ENOENT if the original directory no longer exists" do + dir_name1 = tmp('testdir1') + dir_name2 = tmp('testdir2') + Dir.should_not.exist?(dir_name1) + Dir.should_not.exist?(dir_name2) + Dir.mkdir dir_name1 + Dir.mkdir dir_name2 - dir1 = Dir.new(dir_name1) + dir2 = Dir.new(dir_name2) - begin - -> { - dir1.chdir do - Dir.chdir(dir_name2) { Dir.unlink dir_name1 } + begin + Dir.chdir(dir_name1) do + dir2.chdir { Dir.unlink dir_name1 } end - }.should raise_error(Errno::ENOENT) + Dir.pwd.should == @original + ensure + Dir.unlink dir_name1 if Dir.exist?(dir_name1) + Dir.unlink dir_name2 if Dir.exist?(dir_name2) + end ensure - Dir.unlink dir_name1 if File.exist?(dir_name1) - Dir.unlink dir_name2 if File.exist?(dir_name2) + dir2.close end - ensure - dir1.close end it "always returns to the original directory when given a block" do diff --git a/core/dir/for_fd_spec.rb b/core/dir/for_fd_spec.rb index eeda26754a..3afcc7e949 100644 --- a/core/dir/for_fd_spec.rb +++ b/core/dir/for_fd_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' +quarantine! do # leads to "Errno::EBADF: Bad file descriptor - closedir" in DirSpecs.delete_mock_dirs ruby_version_is '3.3' do guard -> { Dir.respond_to? :for_fd } do describe "Dir.for_fd" do @@ -42,8 +43,8 @@ it "calls #to_int to convert a value to an Integer" do dir = Dir.new(DirSpecs.mock_dir) - obj = Object.new - obj.singleton_class.define_method(:to_int) { dir.fileno } + obj = mock("fd") + obj.should_receive(:to_int).and_return(dir.fileno) dir_new = Dir.for_fd(obj) dir_new.fileno.should == dir.fileno @@ -75,3 +76,4 @@ end end end +end diff --git a/core/exception/fixtures/common.rb b/core/exception/fixtures/common.rb index 1e243098bd..3d8a3c3430 100644 --- a/core/exception/fixtures/common.rb +++ b/core/exception/fixtures/common.rb @@ -84,6 +84,9 @@ class NoMethodErrorD; end class InstanceException < Exception end + + class AClass; end + module AModule; end end class NameErrorSpecs diff --git a/core/exception/no_method_error_spec.rb b/core/exception/no_method_error_spec.rb index 26df3338e9..772c569f67 100644 --- a/core/exception/no_method_error_spec.rb +++ b/core/exception/no_method_error_spec.rb @@ -67,7 +67,7 @@ end ruby_version_is ""..."3.3" do - it "calls receiver.inspect only when calling Exception#message" do + it "calls #inspect when calling Exception#message" do ScratchPad.record [] test_class = Class.new do def inspect @@ -76,19 +76,163 @@ def inspect end end instance = test_class.new + begin instance.bar - rescue Exception => e - e.name.should == :bar - ScratchPad.recorded.should == [] - e.message.should =~ /undefined method.+\bbar\b/ + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for :#$/ ScratchPad.recorded.should == [:inspect_called] end end + + it "fallbacks to a simpler representation of the receiver when receiver.inspect raises an exception" do + test_class = Class.new do + def inspect + raise NoMethodErrorSpecs::InstanceException + end + end + instance = test_class.new + + begin + instance.bar + rescue NoMethodError => error + message = error.message + message.should =~ /undefined method.+\bbar\b/ + message.should include test_class.inspect + end + end + + it "uses #name to display the receiver if it is a class" do + klass = Class.new { def self.name; "MyClass"; end } + + begin + klass.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for MyClass:Class$/ + end + end + + it "uses #name to display the receiver if it is a module" do + mod = Module.new { def self.name; "MyModule"; end } + + begin + mod.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for MyModule:Module$/ + end + end end ruby_version_is "3.3" do - it "does not call receiver.inspect even when calling Exception#message" do + it "uses a literal name when receiver is nil" do + begin + nil.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for nil\Z/ + end + end + + it "uses a literal name when receiver is true" do + begin + true.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for true\Z/ + end + end + + it "uses a literal name when receiver is false" do + begin + false.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for false\Z/ + end + end + + it "uses #name when receiver is a class" do + klass = Class.new { def self.name; "MyClass"; end } + + begin + klass.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for class MyClass\Z/ + end + end + + it "uses class' string representation when receiver is an anonymous class" do + klass = Class.new + + begin + klass.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for class #\Z/ + end + end + + it "uses class' string representation when receiver is a singleton class" do + obj = Object.new + singleton_class = obj.singleton_class + + begin + singleton_class.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for class #>\Z/ + end + end + + it "uses #name when receiver is a module" do + mod = Module.new { def self.name; "MyModule"; end } + + begin + mod.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for module MyModule\Z/ + end + end + + it "uses module's string representation when receiver is an anonymous module" do + m = Module.new + + begin + m.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for module #\Z/ + end + end + + it "uses class #name when receiver is an ordinary object" do + klass = Class.new { def self.name; "MyClass"; end } + instance = klass.new + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for an instance of MyClass\Z/ + end + end + + it "uses class string representation when receiver is an instance of anonymous class" do + klass = Class.new + instance = klass.new + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for an instance of #\Z/ + end + end + + it "uses class name when receiver has a singleton class" do + instance = NoMethodErrorSpecs::NoMethodErrorA.new + def instance.foo; end + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for #\Z/ + end + end + + it "does not call #inspect when calling Exception#message" do ScratchPad.record [] test_class = Class.new do def inspect @@ -97,47 +241,29 @@ def inspect end end instance = test_class.new + begin instance.bar - rescue Exception => e - e.name.should == :bar - ScratchPad.recorded.should == [] - e.message.should =~ /undefined method.+\bbar\b/ + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for an instance of #\Z/ ScratchPad.recorded.should == [] end end - end - it "fallbacks to a simpler representation of the receiver when receiver.inspect raises an exception" do - test_class = Class.new do - def inspect - raise NoMethodErrorSpecs::InstanceException - end - end - instance = test_class.new - begin - instance.bar - rescue Exception => e - e.name.should == :bar - message = e.message - message.should =~ /undefined method.+\bbar\b/ - message.should include test_class.inspect - end - end + it "does not truncate long class names" do + class_name = 'ExceptionSpecs::A' + 'a'*100 - it "uses #name to display the receiver if it is a class or a module" do - klass = Class.new { def self.name; "MyClass"; end } - begin - klass.foo - rescue NoMethodError => error - error.message.lines.first.chomp.should =~ /^undefined method [`']foo' for / - end + begin + eval <<~RUBY + class #{class_name} + end - mod = Module.new { def self.name; "MyModule"; end } - begin - mod.foo - rescue NoMethodError => error - error.message.lines.first.chomp.should =~ /^undefined method [`']foo' for / + obj = #{class_name}.new + obj.foo + RUBY + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for an instance of #{class_name}\Z/ + end end end end diff --git a/core/integer/divide_spec.rb b/core/integer/divide_spec.rb index a878c4668c..665f4d57be 100644 --- a/core/integer/divide_spec.rb +++ b/core/integer/divide_spec.rb @@ -12,6 +12,17 @@ it "supports dividing negative numbers" do (-1 / 10).should == -1 + (-1 / 10**10).should == -1 + (-1 / 10**20).should == -1 + end + + it "preservers sign correctly" do + (4 / 3).should == 1 + (4 / -3).should == -2 + (-4 / 3).should == -2 + (-4 / -3).should == 1 + (0 / -3).should == 0 + (0 / 3).should == 0 end it "returns result the same class as the argument" do @@ -58,6 +69,15 @@ ((10**50) / -(10**40 + 1)).should == -10000000000 end + it "preservers sign correctly" do + (4 / bignum_value).should == 0 + (4 / -bignum_value).should == -1 + (-4 / bignum_value).should == -1 + (-4 / -bignum_value).should == 0 + (0 / bignum_value).should == 0 + (0 / -bignum_value).should == 0 + end + it "returns self divided by Float" do not_supported_on :opal do (bignum_value(88) / 4294967295.0).should be_close(4294967297.0, TOLERANCE) diff --git a/core/kernel/Float_spec.rb b/core/kernel/Float_spec.rb index 0f83cb5824..6cedfe0617 100644 --- a/core/kernel/Float_spec.rb +++ b/core/kernel/Float_spec.rb @@ -157,6 +157,24 @@ def to_f() 1.2 end @object.send(:Float, "1\t\n").should == 1.0 end + ruby_version_is ""..."3.4" do + it "raises ArgumentError if a fractional part is missing" do + -> { @object.send(:Float, "1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "+1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "-1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "1.e+0") }.should raise_error(ArgumentError) + end + end + + ruby_version_is "3.4" do + it "allows String representation without a fractional part" do + @object.send(:Float, "1.").should == 1.0 + @object.send(:Float, "+1.").should == 1.0 + @object.send(:Float, "-1.").should == -1.0 + @object.send(:Float, "1.e+0").should == 1.0 + end + end + %w(e E).each do |e| it "raises an ArgumentError if #{e} is the trailing character" do -> { @object.send(:Float, "2#{e}") }.should raise_error(ArgumentError) @@ -280,7 +298,7 @@ def to_f() 1.2 end nan2.should equal(nan) end - it "returns the identical Infinity if to_f is called and it returns Infinity" do + it "returns the identical Infinity if #to_f is called and it returns Infinity" do infinity = infinity_value (infinity_to_f = mock('Infinity')).should_receive(:to_f).once.and_return(infinity) infinity2 = @object.send(:Float, infinity_to_f) diff --git a/core/marshal/dump_spec.rb b/core/marshal/dump_spec.rb index 05e473c64e..716ba0e946 100644 --- a/core/marshal/dump_spec.rb +++ b/core/marshal/dump_spec.rb @@ -47,6 +47,11 @@ Marshal.dump(-2**31 - 1).should == "\x04\bl-\a\x01\x00\x00\x80" end end + + it "does not use object links for objects repeatedly dumped" do + Marshal.dump([0, 0]).should == "\x04\b[\ai\x00i\x00" + Marshal.dump([2**16, 2**16]).should == "\x04\b[\ai\x03\x00\x00\x01i\x03\x00\x00\x01" + end end describe "with a Symbol" do @@ -93,6 +98,11 @@ value = [*value, value[0]] Marshal.dump(value).should == "\x04\b[\b#{symbol1}#{symbol2};\x00" end + + it "uses symbol links for objects repeatedly dumped" do + symbol = :foo + Marshal.dump([symbol, symbol]).should == "\x04\b[\a:\bfoo;\x00" # ;\x00 is a link to the symbol object + end end describe "with an object responding to #marshal_dump" do @@ -108,6 +118,20 @@ it "raises TypeError if an Object is an instance of an anonymous class" do -> { Marshal.dump(Class.new(UserMarshal).new) }.should raise_error(TypeError, /can't dump anonymous class/) end + + it "uses object links for objects repeatedly dumped" do + obj = UserMarshal.new + Marshal.dump([obj, obj]).should == "\x04\b[\aU:\x10UserMarshal:\tdata@\x06" # @\x06 is a link to the object + end + + it "adds instance variables of a dumped object after the object itself into the objects table" do + value = "" + obj = MarshalSpec::UserMarshalDumpWithIvar.new("string", value) + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\t, that means Integer 4) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bU:)MarshalSpec::UserMarshalDumpWithIvarI[\x06\"\vstring\x06:\t@foo\"\n@\x06@\t" + end end describe "with an object responding to #_dump" do @@ -166,6 +190,20 @@ def _dump(level) Marshal.dump([a, a]).should == "\x04\b[\aIu:\x17MarshalSpec::M1::A\v\x06:\t@foo\"\bbar#{reference}" end + it "uses object links for objects repeatedly dumped" do + obj = UserDefined.new + Marshal.dump([obj, obj]).should == "\x04\b[\au:\x10UserDefined\x12\x04\b[\a:\nstuff;\x00@\x06" # @\x06 is a link to the object + end + + it "adds instance variables of a dumped String before the object itself into the objects table" do + value = "" + obj = MarshalSpec::UserDefinedDumpWithIVars.new(+"string", value) + + # expect a link to the object (@\a, that means Integer 2) is greater than a link + # to the instance variable value (@\x06, that means Integer 1) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:*MarshalSpec::UserDefinedDumpWithIVars\vstring\x06:\t@foo\"\n@\a@\x06" + end + describe "Core library classes with #_dump returning a String with instance variables" do it "indexes instance variables and then a Time object itself" do t = Time.utc(2022) @@ -198,6 +236,10 @@ def _dump(level) Marshal.dump(source_object).should == "\x04\bc,MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Class" end + it "uses object links for objects repeatedly dumped" do + Marshal.dump([String, String]).should == "\x04\b[\ac\vString@\x06" # @\x06 is a link to the object + end + it "raises TypeError with an anonymous Class" do -> { Marshal.dump(Class.new) }.should raise_error(TypeError, /can't dump anonymous class/) end @@ -221,6 +263,10 @@ def _dump(level) Marshal.dump(source_object).should == "\x04\bm-MarshalSpec::Multibyte\xE3\x81\x91\xE3\x81\x92\xE3\x81\x93\xE3\x81\x94Module" end + it "uses object links for objects repeatedly dumped" do + Marshal.dump([Marshal, Marshal]).should == "\x04\b[\am\fMarshal@\x06" # @\x06 is a link to the object + end + it "raises TypeError with an anonymous Module" do -> { Marshal.dump(Module.new) }.should raise_error(TypeError, /can't dump anonymous module/) end @@ -239,6 +285,10 @@ def _dump(level) [Marshal, nan_value, "\004\bf\bnan"], ].should be_computed_by(:dump) end + + it "uses object links for objects repeatedly dumped" do + Marshal.dump([0.0, 0.0]).should == "\x04\b[\af\x060@\x06" # @\x06 is a link to the float value + end end describe "with a Bignum" do @@ -256,6 +306,11 @@ def _dump(level) ].should be_computed_by(:dump) end + it "uses object links for objects repeatedly dumped" do + n = 2**64 + Marshal.dump([n, n]).should == "\x04\b[\al+\n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00@\x06" # @\x06 is a link to the object + end + it "increases the object links counter" do obj = Object.new object_1_link = "\x06" # representing of (0-based) index=1 (by adding 5 for small Integers) @@ -275,12 +330,22 @@ def _dump(level) it "dumps a Rational" do Marshal.dump(Rational(2, 3)).should == "\x04\bU:\rRational[\ai\ai\b" end + + it "uses object links for objects repeatedly dumped" do + r = Rational(2, 3) + Marshal.dump([r, r]).should == "\x04\b[\aU:\rRational[\ai\ai\b@\x06" # @\x06 is a link to the object + end end describe "with a Complex" do it "dumps a Complex" do Marshal.dump(Complex(2, 3)).should == "\x04\bU:\fComplex[\ai\ai\b" end + + it "uses object links for objects repeatedly dumped" do + c = Complex(2, 3) + Marshal.dump([c, c]).should == "\x04\b[\aU:\fComplex[\ai\ai\b@\x06" # @\x06 is a link to the object + end end ruby_version_is "3.2" do @@ -299,6 +364,11 @@ def _dump(level) Marshal.dump(obj).should == "\x04\bS:5MarshalSpec::DataSpec::MeasureWithOverriddenName\a:\vamountii:\tunit\"\akm" end + it "uses object links for objects repeatedly dumped" do + d = MarshalSpec::DataSpec::Measure.new(100, 'km') + Marshal.dump([d, d]).should == "\x04\b[\aS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm@\x06" # @\x06 is a link to the object + end + it "raises TypeError with an anonymous Struct" do -> { Marshal.dump(Data.define(:a).new(1)) }.should raise_error(TypeError, /can't dump anonymous class/) end @@ -360,6 +430,21 @@ def _dump(level) it "dumps multiple strings using symlinks for the :E (encoding) symbol" do Marshal.dump(["".encode("us-ascii"), "".encode("utf-8")]).should == "\x04\b[\aI\"\x00\x06:\x06EFI\"\x00\x06;\x00T" end + + it "uses object links for objects repeatedly dumped" do + s = "string" + Marshal.dump([s, s]).should == "\x04\b[\a\"\vstring@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = +"string" + value = "" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI\"\vstring\x06:\t@foo\"\n@\x06@\a" + end end describe "with a Regexp" do @@ -427,6 +512,11 @@ def _dump(level) obj = MarshalSpec::RegexpWithOverriddenName.new("") Marshal.dump(obj).should == "\x04\bIC:*MarshalSpec::RegexpWithOverriddenName/\x00\x00\x06:\x06EF" end + + it "uses object links for objects repeatedly dumped" do + r = /\A.\Z/ + Marshal.dump([r, r]).should == "\x04\b[\aI/\n\\A.\\Z\x00\x06:\x06EF@\x06" # @\x06 is a link to the object + end end describe "with an Array" do @@ -462,6 +552,21 @@ def _dump(level) obj = MarshalSpec::ArrayWithOverriddenName.new Marshal.dump(obj).should == "\x04\bC:)MarshalSpec::ArrayWithOverriddenName[\x00" end + + it "uses object links for objects repeatedly dumped" do + a = [1] + Marshal.dump([a, a]).should == "\x04\b[\a[\x06i\x06@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = [] + value = "" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI[\x00\x06:\t@foo\"\n@\x06@\a" + end end describe "with a Hash" do @@ -519,6 +624,21 @@ def _dump(level) obj = MarshalSpec::HashWithOverriddenName.new Marshal.dump(obj).should == "\x04\bC:(MarshalSpec::HashWithOverriddenName{\x00" end + + it "uses object links for objects repeatedly dumped" do + h = {a: 1} + Marshal.dump([h, h]).should == "\x04\b[\a{\x06:\x06ai\x06@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = {} + value = "" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI{\x00\x06:\t@foo\"\n@\x06@\a" + end end describe "with a Struct" do @@ -553,9 +673,24 @@ def _dump(level) Marshal.dump(obj).should == "\x04\bS:*MarshalSpec::StructWithOverriddenName\x06:\x06a\"\vmember" end + it "uses object links for objects repeatedly dumped" do + s = Struct::Pyramid.new + Marshal.dump([s, s]).should == "\x04\b[\aS:\x14Struct::Pyramid\x00@\x06" # @\x06 is a link to the object + end + it "raises TypeError with an anonymous Struct" do -> { Marshal.dump(Struct.new(:a).new(1)) }.should raise_error(TypeError, /can't dump anonymous class/) end + + it "adds instance variables after the object itself into the objects table" do + obj = Struct::Pyramid.new + value = "" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIS:\x14Struct::Pyramid\x00\x06:\t@foo\"\n@\x06@\a" + end end describe "with an Object" do @@ -645,21 +780,45 @@ def finalizer.noop(_) ObjectSpace.define_finalizer(obj, finalizer.method(:noop)) Marshal.load(Marshal.dump(obj)).class.should == Object end + + it "uses object links for objects repeatedly dumped" do + obj = Object.new + Marshal.dump([obj, obj]).should == "\x04\b[\ao:\vObject\x00@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = Object.new + value = "" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\vObject\x06:\t@foo\"\n@\x06@\a" + end end describe "with a Range" do - it "dumps a Range inclusive of end (with indeterminant order)" do + it "dumps a Range inclusive of end" do dump = Marshal.dump(1..2) + dump.should == "\x04\bo:\nRange\b:\texclF:\nbegini\x06:\bendi\a" + load = Marshal.load(dump) load.should == (1..2) end - it "dumps a Range exclusive of end (with indeterminant order)" do + it "dumps a Range exclusive of end" do dump = Marshal.dump(1...2) + dump.should == "\x04\bo:\nRange\b:\texclT:\nbegini\x06:\bendi\a" + load = Marshal.load(dump) load.should == (1...2) end + it "uses object links for objects repeatedly dumped" do + r = 1..2 + Marshal.dump([r, r]).should == "\x04\b[\ao:\nRange\b:\texclF:\nbegini\x06:\bendi\a@\x06" # @\x06 is a link to the object + end + it "raises TypeError with an anonymous Range subclass" do -> { Marshal.dump(Class.new(Range).new(1, 2)) }.should raise_error(TypeError, /can't dump anonymous class/) end @@ -711,6 +870,26 @@ def finalizer.noop(_) Marshal.dump(source_object).should == "\x04\bc+MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Time" end + it "uses object links for objects repeatedly dumped" do + # order of the offset and zone instance variables is a subject to change + # and may be different on different CRuby versions + base = Regexp.quote("\x04\b[\aIu:\tTime\r\xF5\xEF\e\x80\x00\x00\x00\x00\a") + offset = Regexp.quote(":\voffseti\x020*:\tzoneI\"\bAST\x06:\x06EF") + zone = Regexp.quote(":\tzoneI\"\bAST\x06:\x06EF:\voffseti\x020*") + instance_variables = /#{offset}|#{zone}/ + Marshal.dump([@t, @t]).should =~ /\A#{base}#{instance_variables}@\a\Z/ # @\a is a link to the object + end + + it "adds instance variables before the object itself into the objects table" do + obj = @utc + value = "" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\b, that means Integer 3) is greater than a link + # to the instance variable value (@\x06, that means Integer 1) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:\tTime\r \x00\x1C\xC0\x00\x00\x00\x00\a:\t@foo\"\n:\tzoneI\"\bUTC\x06:\x06EF@\b@\x06" + end + it "raises TypeError with an anonymous Time subclass" do -> { Marshal.dump(Class.new(Time).now) }.should raise_error(TypeError) end @@ -765,6 +944,21 @@ def finalizer.noop(_) Marshal.dump(e).should =~ /undefined method [`']foo' for ("":String|an instance of String)/ end + it "uses object links for objects repeatedly dumped" do + e = Exception.new + Marshal.dump([e, e]).should == "\x04\b[\ao:\x0EException\a:\tmesg0:\abt0@\x06" # @\x\a is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = Exception.new + value = "" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\x0EException\b:\tmesg0:\abt0:\t@foo\"\n@\x06@\a" + end + it "raises TypeError if an Object is an instance of an anonymous class" do anonymous_class = Class.new(Exception) obj = anonymous_class.new diff --git a/core/marshal/fixtures/marshal_data.rb b/core/marshal/fixtures/marshal_data.rb index 97fd78b2d7..3ba85aa6eb 100644 --- a/core/marshal/fixtures/marshal_data.rb +++ b/core/marshal/fixtures/marshal_data.rb @@ -97,6 +97,25 @@ def self._load(data) end end +module MarshalSpec + class UserDefinedDumpWithIVars + attr_reader :string + + def initialize(string, ivar_value) + @string = string + @string.instance_variable_set(:@foo, ivar_value) + end + + def _dump(depth) + @string + end + + def self._load(data) + new(data) + end + end +end + class UserPreviouslyDefinedWithInitializedIvar attr_accessor :field1, :field2 end @@ -139,6 +158,32 @@ def ==(other) end end +module MarshalSpec + class UserMarshalDumpWithIvar + attr_reader :data + + def initialize(data, ivar_value) + @data = data + @ivar_value = ivar_value + end + + def marshal_dump + obj = [data] + obj.instance_variable_set(:@foo, @ivar_value) + obj + end + + def marshal_load(o) + @data = o[0] + end + + def ==(other) + self.class === other and + @data = other.data + end + end +end + class UserArray < Array end diff --git a/core/method/source_location_spec.rb b/core/method/source_location_spec.rb index c5b296f6e2..23d956ebec 100644 --- a/core/method/source_location_spec.rb +++ b/core/method/source_location_spec.rb @@ -11,23 +11,23 @@ end it "sets the first value to the path of the file in which the method was defined" do - file = @method.source_location.first + file = @method.source_location[0] file.should be_an_instance_of(String) file.should == File.realpath('fixtures/classes.rb', __dir__) end it "sets the last value to an Integer representing the line on which the method was defined" do - line = @method.source_location.last + line = @method.source_location[1] line.should be_an_instance_of(Integer) line.should == 5 end it "returns the last place the method was defined" do - MethodSpecs::SourceLocation.method(:redefined).source_location.last.should == 13 + MethodSpecs::SourceLocation.method(:redefined).source_location[1].should == 13 end it "returns the location of the original method even if it was aliased" do - MethodSpecs::SourceLocation.new.method(:aka).source_location.last.should == 17 + MethodSpecs::SourceLocation.new.method(:aka).source_location[1].should == 17 end it "works for methods defined with a block" do @@ -108,7 +108,13 @@ def f c = Class.new do eval('def self.m; end', nil, "foo", 100) end - c.method(:m).source_location.should == ["foo", 100] + location = c.method(:m).source_location + ruby_version_is(""..."3.5") do + location.should == ["foo", 100] + end + ruby_version_is("3.5") do + location.should == ["foo", 100, 0, 100, 15] + end end describe "for a Method generated by respond_to_missing?" do diff --git a/core/module/fixtures/set_temporary_name.rb b/core/module/fixtures/set_temporary_name.rb new file mode 100644 index 0000000000..901b3b94d1 --- /dev/null +++ b/core/module/fixtures/set_temporary_name.rb @@ -0,0 +1,4 @@ +module ModuleSpecs + module SetTemporaryNameSpec + end +end diff --git a/core/module/set_temporary_name_spec.rb b/core/module/set_temporary_name_spec.rb index f5886a3398..12541374ae 100644 --- a/core/module/set_temporary_name_spec.rb +++ b/core/module/set_temporary_name_spec.rb @@ -1,4 +1,5 @@ require_relative '../../spec_helper' +require_relative 'fixtures/set_temporary_name' ruby_version_is "3.3" do describe "Module#set_temporary_name" do @@ -13,13 +14,34 @@ m.name.should be_nil end + it "returns self" do + m = Module.new + m.set_temporary_name("fake_name").should.equal? m + end + it "can assign a temporary name which is not a valid constant path" do m = Module.new - m.set_temporary_name("a::B") - m.name.should == "a::B" + + m.set_temporary_name("name") + m.name.should == "name" m.set_temporary_name("Template['foo.rb']") m.name.should == "Template['foo.rb']" + + m.set_temporary_name("a::B") + m.name.should == "a::B" + + m.set_temporary_name("A::b") + m.name.should == "A::b" + + m.set_temporary_name("A::B::") + m.name.should == "A::B::" + + m.set_temporary_name("A::::B") + m.name.should == "A::::B" + + m.set_temporary_name("A=") + m.name.should == "A=" end it "can't assign empty string as name" do @@ -43,7 +65,7 @@ -> { Object.set_temporary_name("fake_name") }.should raise_error(RuntimeError, "can't change permanent name") end - it "can assign a temporary name to a nested module" do + it "can assign a temporary name to a module nested into an anonymous module" do m = Module.new module m::N; end m::N.name.should =~ /\A#::N\z/ @@ -55,6 +77,17 @@ module m::N; end m::N.name.should be_nil end + it "discards a temporary name when an outer anonymous module gets a permanent name" do + m = Module.new + module m::N; end + + m::N.set_temporary_name("fake_name") + m::N.name.should == "fake_name" + + ModuleSpecs::SetTemporaryNameSpec::M = m + m::N.name.should == "ModuleSpecs::SetTemporaryNameSpec::M::N" + end + it "can update the name when assigned to a constant" do m = Module.new m::N = Module.new @@ -64,5 +97,45 @@ module m::N; end m::M = m::N m::M.name.should =~ /\A#::M\z/m end + + it "can reassign a temporary name repeatedly" do + m = Module.new + + m.set_temporary_name("fake_name") + m.name.should == "fake_name" + + m.set_temporary_name("fake_name_2") + m.name.should == "fake_name_2" + end + + it "does not affect a name of a module nested into an anonymous module with a temporary name" do + m = Module.new + m::N = Module.new + m::N.name.should =~ /\A#::N\z/ + + m.set_temporary_name("foo") + m::N.name.should =~ /\A#::N\z/ + end + + it "keeps temporary name when assigned in an anonymous module" do + outer = Module.new + m = Module.new + m.set_temporary_name "m" + m.name.should == "m" + outer::M = m + m.name.should == "m" + m.inspect.should == "m" + end + + it "keeps temporary name when assigned in an anonymous module and nested before" do + outer = Module.new + m = Module.new + outer::A = m + m.set_temporary_name "m" + m.name.should == "m" + outer::M = m + m.name.should == "m" + m.inspect.should == "m" + end end end diff --git a/core/proc/clone_spec.rb b/core/proc/clone_spec.rb index 26f031334f..730dc421a8 100644 --- a/core/proc/clone_spec.rb +++ b/core/proc/clone_spec.rb @@ -15,7 +15,7 @@ end ruby_version_is "3.3" do - it "calls #initialize_copy on subclass" do + it "calls #initialize_clone on subclass" do obj = ProcSpecs::MyProc2.new(:a, 2) { } dup = obj.clone @@ -24,7 +24,7 @@ dup.first.should == :a dup.second.should == 2 - dup.initializer.should == :copy + dup.initializer.should == :clone end end end diff --git a/core/proc/fixtures/common.rb b/core/proc/fixtures/common.rb index 204acde597..dfe67d7ba8 100644 --- a/core/proc/fixtures/common.rb +++ b/core/proc/fixtures/common.rb @@ -47,6 +47,13 @@ def initialize_dup(other) @first = other.first @second = other.second end + + def initialize_clone(other, **options) + super + @initializer = :clone + @first = other.first + @second = other.second + end end class Arity diff --git a/core/proc/source_location_spec.rb b/core/proc/source_location_spec.rb index a8b99287d5..484466f577 100644 --- a/core/proc/source_location_spec.rb +++ b/core/proc/source_location_spec.rb @@ -17,57 +17,64 @@ end it "sets the first value to the path of the file in which the proc was defined" do - file = @proc.source_location.first + file = @proc.source_location[0] file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @proc_new.source_location.first + file = @proc_new.source_location[0] file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @lambda.source_location.first + file = @lambda.source_location[0] file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @method.source_location.first + file = @method.source_location[0] file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) end - it "sets the last value to an Integer representing the line on which the proc was defined" do - line = @proc.source_location.last + it "sets the second value to an Integer representing the line on which the proc was defined" do + line = @proc.source_location[1] line.should be_an_instance_of(Integer) line.should == 4 - line = @proc_new.source_location.last + line = @proc_new.source_location[1] line.should be_an_instance_of(Integer) line.should == 12 - line = @lambda.source_location.last + line = @lambda.source_location[1] line.should be_an_instance_of(Integer) line.should == 8 - line = @method.source_location.last + line = @method.source_location[1] line.should be_an_instance_of(Integer) line.should == 15 end it "works even if the proc was created on the same line" do - proc { true }.source_location.should == [__FILE__, __LINE__] - Proc.new { true }.source_location.should == [__FILE__, __LINE__] - -> { true }.source_location.should == [__FILE__, __LINE__] + ruby_version_is(""..."3.5") do + proc { true }.source_location.should == [__FILE__, __LINE__] + Proc.new { true }.source_location.should == [__FILE__, __LINE__] + -> { true }.source_location.should == [__FILE__, __LINE__] + end + ruby_version_is("3.5") do + proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19] + Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23] + -> { true }.source_location.should == [__FILE__, __LINE__, 8, __LINE__, 17] + end end it "returns the first line of a multi-line proc (i.e. the line containing 'proc do')" do - ProcSpecs::SourceLocation.my_multiline_proc.source_location.last.should == 20 - ProcSpecs::SourceLocation.my_multiline_proc_new.source_location.last.should == 34 - ProcSpecs::SourceLocation.my_multiline_lambda.source_location.last.should == 27 + ProcSpecs::SourceLocation.my_multiline_proc.source_location[1].should == 20 + ProcSpecs::SourceLocation.my_multiline_proc_new.source_location[1].should == 34 + ProcSpecs::SourceLocation.my_multiline_lambda.source_location[1].should == 27 end it "returns the location of the proc's body; not necessarily the proc itself" do - ProcSpecs::SourceLocation.my_detached_proc.source_location.last.should == 41 - ProcSpecs::SourceLocation.my_detached_proc_new.source_location.last.should == 51 - ProcSpecs::SourceLocation.my_detached_lambda.source_location.last.should == 46 + ProcSpecs::SourceLocation.my_detached_proc.source_location[1].should == 41 + ProcSpecs::SourceLocation.my_detached_proc_new.source_location[1].should == 51 + ProcSpecs::SourceLocation.my_detached_lambda.source_location[1].should == 46 end it "returns the same value for a proc-ified method as the method reports" do @@ -86,6 +93,12 @@ it "works for eval with a given line" do proc = eval('-> {}', nil, "foo", 100) - proc.source_location.should == ["foo", 100] + location = proc.source_location + ruby_version_is(""..."3.5") do + location.should == ["foo", 100] + end + ruby_version_is("3.5") do + location.should == ["foo", 100, 2, 100, 5] + end end end diff --git a/core/range/overlap_spec.rb b/core/range/overlap_spec.rb new file mode 100644 index 0000000000..9b6fc13493 --- /dev/null +++ b/core/range/overlap_spec.rb @@ -0,0 +1,89 @@ +require_relative '../../spec_helper' + +ruby_version_is '3.3' do + describe "Range#overlap?" do + it "returns true if other Range overlaps self" do + (0..2).overlap?(1..3).should == true + (1..3).overlap?(0..2).should == true + (0..2).overlap?(0..2).should == true + (0..3).overlap?(1..2).should == true + (1..2).overlap?(0..3).should == true + + ('a'..'c').overlap?('b'..'d').should == true + end + + it "returns false if other Range does not overlap self" do + (0..2).overlap?(3..4).should == false + (0..2).overlap?(-4..-1).should == false + + ('a'..'c').overlap?('d'..'f').should == false + end + + it "raises TypeError when called with non-Range argument" do + -> { + (0..2).overlap?(1) + }.should raise_error(TypeError, "wrong argument type Integer (expected Range)") + end + + it "returns true when beginningless and endless Ranges overlap" do + (0..2).overlap?(..3).should == true + (0..2).overlap?(..1).should == true + (0..2).overlap?(..0).should == true + + (..3).overlap?(0..2).should == true + (..1).overlap?(0..2).should == true + (..0).overlap?(0..2).should == true + + (0..2).overlap?(-1..).should == true + (0..2).overlap?(1..).should == true + (0..2).overlap?(2..).should == true + + (-1..).overlap?(0..2).should == true + (1..).overlap?(0..2).should == true + (2..).overlap?(0..2).should == true + + (0..).overlap?(2..).should == true + (..0).overlap?(..2).should == true + end + + it "returns false when beginningless and endless Ranges do not overlap" do + (0..2).overlap?(..-1).should == false + (0..2).overlap?(3..).should == false + + (..-1).overlap?(0..2).should == false + (3..).overlap?(0..2).should == false + end + + it "returns false when Ranges are not compatible" do + (0..2).overlap?('a'..'d').should == false + end + + it "return false when self is empty" do + (2..0).overlap?(1..3).should == false + (2...2).overlap?(1..3).should == false + (1...1).overlap?(1...1).should == false + (2..0).overlap?(2..0).should == false + + ('c'..'a').overlap?('b'..'d').should == false + ('a'...'a').overlap?('b'..'d').should == false + ('b'...'b').overlap?('b'...'b').should == false + ('c'...'a').overlap?('c'...'a').should == false + end + + it "return false when other Range is empty" do + (1..3).overlap?(2..0).should == false + (1..3).overlap?(2...2).should == false + + ('b'..'d').overlap?('c'..'a').should == false + ('b'..'d').overlap?('c'...'c').should == false + end + + it "takes into account exclusive end" do + (0...2).overlap?(2..4).should == false + (2..4).overlap?(0...2).should == false + + ('a'...'c').overlap?('c'..'e').should == false + ('c'..'e').overlap?('a'...'c').should == false + end + end +end diff --git a/core/string/modulo_spec.rb b/core/string/modulo_spec.rb index 8e3853551f..33c2141812 100644 --- a/core/string/modulo_spec.rb +++ b/core/string/modulo_spec.rb @@ -749,9 +749,11 @@ def obj.to_s() "obj" end (format % "-10.4e-20").should == (format % -10.4e-20) (format % ".5").should == (format % 0.5) (format % "-.5").should == (format % -0.5) - ruby_bug("#20705", ""..."3.4") do + + ruby_version_is "3.4" do (format % "10.").should == (format % 10) end + # Something's strange with this spec: # it works just fine in individual mode, but not when run as part of a group (format % "10_1_0.5_5_5").should == (format % 1010.555) diff --git a/core/time/at_spec.rb b/core/time/at_spec.rb index 48fb3c6f52..85bb6d7ebf 100644 --- a/core/time/at_spec.rb +++ b/core/time/at_spec.rb @@ -228,6 +228,12 @@ time.utc_offset.should == -9*60*60 time.zone.should == nil time.to_i.should == @epoch_time + + time = Time.at(@epoch_time, in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil + time.to_i.should == @epoch_time end it "could be UTC offset as a number of seconds" do @@ -280,5 +286,51 @@ -> { Time.at(@epoch_time, in: "+09:99") }.should raise_error(ArgumentError) -> { Time.at(@epoch_time, in: "ABC") }.should raise_error(ArgumentError) end + + it "raises ArgumentError if hours greater than 23" do # TODO + ruby_version_is ""..."3.1" do + -> { Time.at(@epoch_time, in: "+24:00") }.should raise_error(ArgumentError, 'utc_offset out of range') + -> { Time.at(@epoch_time, in: "+2400") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.at(@epoch_time, in: "+99:00") }.should raise_error(ArgumentError, 'utc_offset out of range') + -> { Time.at(@epoch_time, in: "+9900") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.at(@epoch_time, in: "+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.at(@epoch_time, in: "+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.at(@epoch_time, in: "+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.at(@epoch_time, in: "+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + + it "raises ArgumentError if minutes greater than 59" do # TODO + ruby_version_is ""..."3.1" do + -> { Time.at(@epoch_time, in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.at(@epoch_time, in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.at(@epoch_time, in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.at(@epoch_time, in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.at(@epoch_time, in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.at(@epoch_time, in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.at(@epoch_time, in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.at(@epoch_time, in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if seconds greater than 59" do + -> { Time.at(@epoch_time, in: "+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.at(@epoch_time, in: "+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.at(@epoch_time, in: "+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.at(@epoch_time, in: "+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end end end diff --git a/core/time/getlocal_spec.rb b/core/time/getlocal_spec.rb index 926a6dbf45..ff3e3d8dd1 100644 --- a/core/time/getlocal_spec.rb +++ b/core/time/getlocal_spec.rb @@ -59,12 +59,24 @@ t.utc_offset.should == 3600 end + it "returns a Time with a UTC offset specified as +HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("+01:00:01") + t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601) + t.utc_offset.should == 3601 + end + it "returns a Time with a UTC offset specified as -HH:MM" do t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00") t.should == Time.new(2007, 1, 9, 11, 0, 0, -3600) t.utc_offset.should == -3600 end + it "returns a Time with a UTC offset specified as -HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00:01") + t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601) + t.utc_offset.should == -3601 + end + describe "with an argument that responds to #to_str" do it "coerces using #to_str" do o = mock('string') @@ -97,6 +109,52 @@ -> { t.getlocal(86400) }.should raise_error(ArgumentError) end + it "raises ArgumentError if String argument and hours greater than 23" do + ruby_version_is ""..."3.1" do + -> { Time.now.getlocal("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+2400") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.getlocal("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+9900") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.getlocal("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now.getlocal("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + ruby_version_is ""..."3.1" do + -> { Time.now.getlocal("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.getlocal("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.getlocal("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.getlocal("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.getlocal("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now.getlocal("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now.getlocal("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now.getlocal("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now.getlocal("+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now.getlocal("+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now.getlocal("+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now.getlocal("+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + describe "with a timezone argument" do it "returns a Time in the timezone" do zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) diff --git a/core/time/localtime_spec.rb b/core/time/localtime_spec.rb index 609b6532a1..5badba9fb2 100644 --- a/core/time/localtime_spec.rb +++ b/core/time/localtime_spec.rb @@ -72,6 +72,13 @@ t.utc_offset.should == 3600 end + it "returns a Time with a UTC offset specified as +HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("+01:00:01") + t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601) + t.utc_offset.should == 3601 + end + it "returns a Time with a UTC offset specified as -HH:MM" do t = Time.gm(2007, 1, 9, 12, 0, 0) t.localtime("-01:00") @@ -79,6 +86,13 @@ t.utc_offset.should == -3600 end + it "returns a Time with a UTC offset specified as -HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("-01:00:01") + t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601) + t.utc_offset.should == -3601 + end + it "returns a Time with a UTC offset specified as UTC" do t = Time.new(2007, 1, 9, 12, 0, 0, 3600) t.localtime("UTC") @@ -91,6 +105,52 @@ t.utc_offset.should == 3600 * 2 end + it "raises ArgumentError if String argument and hours greater than 23" do + ruby_version_is ""..."3.1" do + -> { Time.now.localtime("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+2400") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.localtime("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+9900") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.localtime("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now.localtime("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + ruby_version_is ""..."3.1" do + -> { Time.now.localtime("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.localtime("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.localtime("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.localtime("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.localtime("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now.localtime("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now.localtime("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now.localtime("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now.localtime("+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now.localtime("+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now.localtime("+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now.localtime("+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + platform_is_not :windows do it "changes the timezone according to the set one" do t = Time.new(2005, 2, 27, 22, 50, 0, -3600) diff --git a/core/time/new_spec.rb b/core/time/new_spec.rb index 24bb9fde0c..1a743b485e 100644 --- a/core/time/new_spec.rb +++ b/core/time/new_spec.rb @@ -415,6 +415,11 @@ def zone.local_to_utc(t) time.utc_offset.should == -9*60*60 time.zone.should == nil + + time = Time.new(2000, 1, 1, 12, 0, 0, in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil end it "could be UTC offset as a number of seconds" do @@ -659,7 +664,7 @@ def obj.to_int; 3; end -> { Time.new("2020-12-25 00:56:17 +23:61") - }.should raise_error(ArgumentError, /utc_offset|can't parse:/) + }.should raise_error(ArgumentError, /utc_offset/) ruby_bug '#20797', ''...'3.4' do -> { @@ -668,6 +673,28 @@ def obj.to_int; 3; end end end + it "raises ArgumentError if utc offset parts are not valid" do + -> { Time.new("2020-12-25 00:56:17 +24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.new("2020-12-25 00:56:17 +2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.new("2020-12-25 00:56:17 +99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.new("2020-12-25 00:56:17 +9900") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.new("2020-12-25 00:56:17 +00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.new("2020-12-25 00:56:17 +0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.new("2020-12-25 00:56:17 +00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.new("2020-12-25 00:56:17 +0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + + ruby_bug '#20797', ''...'3.4' do + -> { Time.new("2020-12-25 00:56:17 +00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.new("2020-12-25 00:56:17 +000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.new("2020-12-25 00:56:17 +00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.new("2020-12-25 00:56:17 +000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + it "raises ArgumentError if string has not ascii-compatible encoding" do -> { Time.new("2021-11-31 00:00:60 +09:00".encode("utf-32le")) diff --git a/core/time/now_spec.rb b/core/time/now_spec.rb index d47f00723e..7c2425411a 100644 --- a/core/time/now_spec.rb +++ b/core/time/now_spec.rb @@ -16,6 +16,11 @@ time.utc_offset.should == -9*60*60 time.zone.should == nil + + time = Time.now(in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil end it "could be UTC offset as a number of seconds" do @@ -52,6 +57,32 @@ -> { Time.now(in: "+09:99") }.should raise_error(ArgumentError) -> { Time.now(in: "ABC") }.should raise_error(ArgumentError) end + + it "raises ArgumentError if String argument and hours greater than 23" do + -> { Time.now(in: "+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now(in: "+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now(in: "+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now(in: "+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + -> { Time.now(in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now(in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now(in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now(in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now(in: "+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now(in: "+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now(in: "+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now(in: "+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end end end end diff --git a/core/unboundmethod/source_location_spec.rb b/core/unboundmethod/source_location_spec.rb index 5c2f14362c..45bd69438c 100644 --- a/core/unboundmethod/source_location_spec.rb +++ b/core/unboundmethod/source_location_spec.rb @@ -7,23 +7,23 @@ end it "sets the first value to the path of the file in which the method was defined" do - file = @method.source_location.first + file = @method.source_location[0] file.should be_an_instance_of(String) file.should == File.realpath('fixtures/classes.rb', __dir__) end - it "sets the last value to an Integer representing the line on which the method was defined" do - line = @method.source_location.last + it "sets the second value to an Integer representing the line on which the method was defined" do + line = @method.source_location[1] line.should be_an_instance_of(Integer) line.should == 5 end it "returns the last place the method was defined" do - UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location.last.should == 13 + UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location[1].should == 13 end it "returns the location of the original method even if it was aliased" do - UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location.last.should == 17 + UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location[1].should == 17 end it "works for define_method methods" do @@ -54,6 +54,12 @@ c = Class.new do eval('def m; end', nil, "foo", 100) end - c.instance_method(:m).source_location.should == ["foo", 100] - end + location = c.instance_method(:m).source_location + ruby_version_is(""..."3.5") do + location.should == ["foo", 100] + end + ruby_version_is("3.5") do + location.should == ["foo", 100, 0, 100, 10] + end + end end diff --git a/library/irb/fixtures/irb.rb b/library/irb/fixtures/irb.rb new file mode 100644 index 0000000000..8d386dfda1 --- /dev/null +++ b/library/irb/fixtures/irb.rb @@ -0,0 +1,3 @@ +a = 10 + +binding.irb # rubocop:disable Lint/Debugger diff --git a/core/binding/irb_spec.rb b/library/irb/irb_spec.rb similarity index 100% rename from core/binding/irb_spec.rb rename to library/irb/irb_spec.rb diff --git a/library/rubygems/gem/bin_path_spec.rb b/library/rubygems/gem/bin_path_spec.rb index 67b3e042c2..0b8c4db08b 100644 --- a/library/rubygems/gem/bin_path_spec.rb +++ b/library/rubygems/gem/bin_path_spec.rb @@ -22,6 +22,7 @@ end skip "Could not find the default gemspecs" unless Dir.exist?(default_specifications_dir) + skip "default_specifications_dir mismatch with GEM_HOME" if ENV["GEM_HOME"] && !default_specifications_dir.start_with?(ENV['GEM_HOME']) Gem::Specification.each_spec([default_specifications_dir]) do |spec| spec.executables.each do |exe| diff --git a/library/socket/basicsocket/recv_nonblock_spec.rb b/library/socket/basicsocket/recv_nonblock_spec.rb index df42c116fb..5bdbbfe688 100644 --- a/library/socket/basicsocket/recv_nonblock_spec.rb +++ b/library/socket/basicsocket/recv_nonblock_spec.rb @@ -99,3 +99,68 @@ end end end + +describe "Socket::BasicSocket#recv_nonblock" do + context "when recvfrom(2) returns 0 (if no messages are available to be received and the peer has performed an orderly shutdown)" do + describe "stream socket" do + before :each do + @server = TCPServer.new('127.0.0.1', 0) + @port = @server.addr[1] + end + + after :each do + @server.close unless @server.closed? + end + + ruby_version_is ""..."3.3" do + quarantine! do # May fail with "IO::EAGAINWaitReadable: Resource temporarily unavailable - recvfrom(2) would block" error + it "returns an empty String on a closed stream socket" do + ready = false + + t = Thread.new do + client = @server.accept + + Thread.pass while !ready + client.recv_nonblock(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + ready = true + + t.value.should == "" + end + end + end + + ruby_version_is "3.3" do + it "returns nil on a closed stream socket" do + ready = false + + t = Thread.new do + client = @server.accept + + Thread.pass while !ready + client.recv_nonblock(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + ready = true + + t.value.should be_nil + end + end + end + end +end diff --git a/library/socket/basicsocket/recv_spec.rb b/library/socket/basicsocket/recv_spec.rb index e82a357d3d..e20c571922 100644 --- a/library/socket/basicsocket/recv_spec.rb +++ b/library/socket/basicsocket/recv_spec.rb @@ -32,25 +32,6 @@ ScratchPad.recorded.should == 'hello' end - ruby_version_is "3.3" do - it "returns nil on a closed stream socket" do - t = Thread.new do - client = @server.accept - packet = client.recv(10) - client.close - packet - end - - Thread.pass while t.status and t.status != "sleep" - t.status.should_not be_nil - - socket = TCPSocket.new('127.0.0.1', @port) - socket.close - - t.value.should be_nil - end - end - platform_is_not :solaris do it "accepts flags to specify unusual receiving behaviour" do t = Thread.new do @@ -192,3 +173,80 @@ end end end + +describe "BasicSocket#recv" do + context "when recvfrom(2) returns 0 (if no messages are available to be received and the peer has performed an orderly shutdown)" do + describe "stream socket" do + before :each do + @server = TCPServer.new('127.0.0.1', 0) + @port = @server.addr[1] + end + + after :each do + @server.close unless @server.closed? + end + + ruby_version_is ""..."3.3" do + it "returns an empty String on a closed stream socket" do + t = Thread.new do + client = @server.accept + client.recv(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + + t.value.should == "" + end + end + + ruby_version_is "3.3" do + it "returns nil on a closed stream socket" do + t = Thread.new do + client = @server.accept + client.recv(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + + t.value.should be_nil + end + end + end + + describe "datagram socket" do + SocketSpecs.each_ip_protocol do |family, ip_address| + before :each do + @server = UDPSocket.new(family) + @client = UDPSocket.new(family) + end + + after :each do + @server.close unless @server.closed? + @client.close unless @client.closed? + end + + it "returns empty String" do + @server.bind(ip_address, 0) + addr = @server.connect_address + @client.connect(addr.ip_address, addr.ip_port) + + @client.send('', 0) + + @server.recv(1).should == "" + end + end + end + end +end diff --git a/library/socket/basicsocket/recvmsg_nonblock_spec.rb b/library/socket/basicsocket/recvmsg_nonblock_spec.rb index cc4275c417..8d5f541a8e 100644 --- a/library/socket/basicsocket/recvmsg_nonblock_spec.rb +++ b/library/socket/basicsocket/recvmsg_nonblock_spec.rb @@ -222,3 +222,71 @@ end end end + +describe 'BasicSocket#recvmsg_nonblock' do + context "when recvfrom(2) returns 0 (if no messages are available to be received and the peer has performed an orderly shutdown)" do + describe "stream socket" do + before :each do + @server = TCPServer.new('127.0.0.1', 0) + @port = @server.addr[1] + end + + after :each do + @server.close unless @server.closed? + end + + ruby_version_is ""..."3.3" do + quarantine! do # May fail with "IO::EAGAINWaitReadable: Resource temporarily unavailable - recvfrom(2) would block" error + it "returns an empty String as received data on a closed stream socket" do + ready = false + + t = Thread.new do + client = @server.accept + + Thread.pass while !ready + client.recvmsg_nonblock(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + ready = true + + t.value.should.is_a? Array + t.value[0].should == "" + end + end + end + + ruby_version_is "3.3" do + platform_is_not :windows do + it "returns nil on a closed stream socket" do + ready = false + + t = Thread.new do + client = @server.accept + + Thread.pass while !ready + client.recvmsg_nonblock(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + ready = true + + t.value.should be_nil + end + end + end + end + end +end diff --git a/library/socket/basicsocket/recvmsg_spec.rb b/library/socket/basicsocket/recvmsg_spec.rb index 8063723701..04ba1d74c7 100644 --- a/library/socket/basicsocket/recvmsg_spec.rb +++ b/library/socket/basicsocket/recvmsg_spec.rb @@ -195,3 +195,87 @@ end end end + +describe 'BasicSocket#recvmsg' do + context "when recvfrom(2) returns 0 (if no messages are available to be received and the peer has performed an orderly shutdown)" do + describe "stream socket" do + before :each do + @server = TCPServer.new('127.0.0.1', 0) + @port = @server.addr[1] + end + + after :each do + @server.close unless @server.closed? + end + + ruby_version_is ""..."3.3" do + platform_is_not :windows do + it "returns an empty String as received data on a closed stream socket" do + t = Thread.new do + client = @server.accept + client.recvmsg(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + + t.value.should.is_a? Array + t.value[0].should == "" + end + end + end + + ruby_version_is "3.3" do + platform_is_not :windows do + it "returns nil on a closed stream socket" do + t = Thread.new do + client = @server.accept + client.recvmsg(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + socket = TCPSocket.new('127.0.0.1', @port) + socket.close + + t.value.should be_nil + end + end + end + end + + describe "datagram socket" do + SocketSpecs.each_ip_protocol do |family, ip_address| + before :each do + @server = UDPSocket.new(family) + @client = UDPSocket.new(family) + end + + after :each do + @server.close unless @server.closed? + @client.close unless @client.closed? + end + + it "returns an empty String as received data" do + @server.bind(ip_address, 0) + addr = @server.connect_address + @client.connect(addr.ip_address, addr.ip_port) + + @client.send('', 0) + message = @server.recvmsg(1) + + message.should.is_a? Array + message[0].should == "" + end + end + end + end +end diff --git a/library/socket/ipsocket/recvfrom_spec.rb b/library/socket/ipsocket/recvfrom_spec.rb index 2af86ea70d..b58903df23 100644 --- a/library/socket/ipsocket/recvfrom_spec.rb +++ b/library/socket/ipsocket/recvfrom_spec.rb @@ -69,6 +69,88 @@ end end +describe "Socket::IPSocket#recvfrom" do + context "when recvfrom(2) returns 0 (if no messages are available to be received and the peer has performed an orderly shutdown)" do + describe "stream socket" do + before :each do + @server = TCPServer.new("127.0.0.1", 0) + port = @server.addr[1] + @client = TCPSocket.new("127.0.0.1", port) + end + + after :each do + @server.close unless @server.closed? + @client.close unless @client.closed? + end + + ruby_version_is ""..."3.3" do + it "returns an empty String as received data on a closed stream socket" do + t = Thread.new do + client = @server.accept + message = client.recvfrom(10) + message + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + @client.close + + t.value.should.is_a? Array + t.value[0].should == "" + end + end + + ruby_version_is "3.3" do + it "returns nil on a closed stream socket" do + t = Thread.new do + client = @server.accept + message = client.recvfrom(10) + message + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + @client.close + + t.value.should be_nil + end + end + end + + describe "datagram socket" do + SocketSpecs.each_ip_protocol do |family, ip_address| + before :each do + @server = UDPSocket.new(family) + @client = UDPSocket.new(family) + end + + after :each do + @server.close unless @server.closed? + @client.close unless @client.closed? + end + + it "returns an empty String as received data" do + @server.bind(ip_address, 0) + addr = @server.connect_address + @client.connect(addr.ip_address, addr.ip_port) + + @client.send('', 0) + message = @server.recvfrom(1) + + message.should.is_a? Array + message[0].should == "" + end + end + end + end +end + describe 'Socket::IPSocket#recvfrom' do SocketSpecs.each_ip_protocol do |family, ip_address, family_name| before do diff --git a/library/socket/socket/gethostname_spec.rb b/library/socket/socket/gethostname_spec.rb index 4b79747b27..89e1ed496f 100644 --- a/library/socket/socket/gethostname_spec.rb +++ b/library/socket/socket/gethostname_spec.rb @@ -2,7 +2,15 @@ require_relative '../fixtures/classes' describe "Socket.gethostname" do + def system_hostname + # Most platforms implement this POSIX standard: + `uname -n`.strip + rescue + # Only really required for Windows without MSYS/MinGW/Cygwin etc: + `hostname`.strip + end + it "returns the host name" do - Socket.gethostname.should == `hostname`.strip + Socket.gethostname.should == system_hostname end end diff --git a/library/socket/socket/recvfrom_nonblock_spec.rb b/library/socket/socket/recvfrom_nonblock_spec.rb index 5596f91bb8..ade655e443 100644 --- a/library/socket/socket/recvfrom_nonblock_spec.rb +++ b/library/socket/socket/recvfrom_nonblock_spec.rb @@ -137,3 +137,79 @@ end end end + +describe 'Socket#recvfrom_nonblock' do + context "when recvfrom(2) returns 0 (if no messages are available to be received and the peer has performed an orderly shutdown)" do + describe "stream socket" do + before :each do + @server = Socket.new Socket::AF_INET, :STREAM, 0 + @sockaddr = Socket.sockaddr_in(0, "127.0.0.1") + @server.bind(@sockaddr) + @server.listen(1) + + server_ip = @server.local_address.ip_port + @server_addr = Socket.sockaddr_in(server_ip, "127.0.0.1") + + @client = Socket.new(Socket::AF_INET, :STREAM, 0) + end + + after :each do + @server.close unless @server.closed? + @client.close unless @client.closed? + end + + ruby_version_is ""..."3.3" do + quarantine! do # May fail with "IO::EAGAINWaitReadable: Resource temporarily unavailable - recvfrom(2) would block" error + it "returns an empty String as received data on a closed stream socket" do + ready = false + + t = Thread.new do + client, _ = @server.accept + + Thread.pass while !ready + client.recvfrom_nonblock(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + @client.connect(@server_addr) + @client.close + ready = true + + t.value.should.is_a? Array + t.value[0].should == "" + end + end + end + + ruby_version_is "3.3" do + quarantine! do # May fail with "IO::EAGAINWaitReadable: Resource temporarily unavailable - recvfrom(2) would block" error + it "returns nil on a closed stream socket" do + ready = false + + t = Thread.new do + client, _ = @server.accept + + Thread.pass while !ready + client.recvfrom_nonblock(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + @client.connect(@server_addr) + @client.close + ready = true + + t.value.should be_nil + end + end + end + end + end +end diff --git a/library/socket/socket/recvfrom_spec.rb b/library/socket/socket/recvfrom_spec.rb index faf161e4a5..6ba39ffcaf 100644 --- a/library/socket/socket/recvfrom_spec.rb +++ b/library/socket/socket/recvfrom_spec.rb @@ -90,3 +90,90 @@ end end end + +describe 'Socket#recvfrom' do + context "when recvfrom(2) returns 0 (if no messages are available to be received and the peer has performed an orderly shutdown)" do + describe "stream socket" do + before :each do + @server = Socket.new Socket::AF_INET, :STREAM, 0 + sockaddr = Socket.sockaddr_in(0, "127.0.0.1") + @server.bind(sockaddr) + @server.listen(1) + + server_ip = @server.local_address.ip_port + @server_addr = Socket.sockaddr_in(server_ip, "127.0.0.1") + + @client = Socket.new(Socket::AF_INET, :STREAM, 0) + end + + after :each do + @server.close unless @server.closed? + @client.close unless @client.closed? + end + + ruby_version_is ""..."3.3" do + it "returns an empty String as received data on a closed stream socket" do + t = Thread.new do + client, _ = @server.accept + client.recvfrom(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + @client.connect(@server_addr) + @client.close + + t.value.should.is_a? Array + t.value[0].should == "" + end + end + + ruby_version_is "3.3" do + it "returns nil on a closed stream socket" do + t = Thread.new do + client, _ = @server.accept + client.recvfrom(10) + ensure + client.close if client + end + + Thread.pass while t.status and t.status != "sleep" + t.status.should_not be_nil + + @client.connect(@server_addr) + @client.close + + t.value.should be_nil + end + end + end + + describe "datagram socket" do + SocketSpecs.each_ip_protocol do |family, ip_address| + before :each do + @server = Socket.new(family, :DGRAM) + @client = Socket.new(family, :DGRAM) + end + + after :each do + @server.close unless @server.closed? + @client.close unless @client.closed? + end + + it "returns an empty String as received data" do + @server.bind(Socket.sockaddr_in(0, ip_address)) + @client.connect(@server.getsockname) + + @client.send('', 0) + message = @server.recvfrom(1) + + message.should.is_a? Array + message[0].should == "" + end + end + end + end +end diff --git a/library/yaml/dump_spec.rb b/library/yaml/dump_spec.rb index ea94b2f856..97b665d6a5 100644 --- a/library/yaml/dump_spec.rb +++ b/library/yaml/dump_spec.rb @@ -39,7 +39,11 @@ end it "dumps an OpenStruct" do - require "ostruct" + begin + require "ostruct" + rescue LoadError + skip "OpenStruct is not available" + end os = OpenStruct.new("age" => 20, "name" => "John") yaml_dump = YAML.dump(os) diff --git a/library/yaml/shared/load.rb b/library/yaml/shared/load.rb index bd55332fe5..b8bb605b0a 100644 --- a/library/yaml/shared/load.rb +++ b/library/yaml/shared/load.rb @@ -123,7 +123,11 @@ end it "loads an OpenStruct" do - require "ostruct" + begin + require "ostruct" + rescue LoadError + skip "OpenStruct is not available" + end os = OpenStruct.new("age" => 20, "name" => "John") loaded = YAML.send(@method, "--- !ruby/object:OpenStruct\ntable:\n :age: 20\n :name: John\n") loaded.should == os diff --git a/optional/capi/ext/io_spec.c b/optional/capi/ext/io_spec.c index d1cc861e21..3b7e8c4481 100644 --- a/optional/capi/ext/io_spec.c +++ b/optional/capi/ext/io_spec.c @@ -377,16 +377,14 @@ static VALUE io_spec_rb_io_closed_p(VALUE self, VALUE io) { } static VALUE io_spec_rb_io_open_descriptor(VALUE self, VALUE klass, VALUE descriptor, VALUE mode, VALUE path, VALUE timeout, VALUE internal_encoding, VALUE external_encoding, VALUE ecflags, VALUE ecopts) { - struct rb_io_encoding *io_encoding; + struct rb_io_encoding io_encoding; - io_encoding = (struct rb_io_encoding *) malloc(sizeof(struct rb_io_encoding)); + io_encoding.enc = rb_to_encoding(internal_encoding); + io_encoding.enc2 = rb_to_encoding(external_encoding); + io_encoding.ecflags = FIX2INT(ecflags); + io_encoding.ecopts = ecopts; - io_encoding->enc = rb_to_encoding(internal_encoding); - io_encoding->enc2 = rb_to_encoding(external_encoding); - io_encoding->ecflags = FIX2INT(ecflags); - io_encoding->ecopts = ecopts; - - return rb_io_open_descriptor(klass, FIX2INT(descriptor), FIX2INT(mode), path, timeout, io_encoding); + return rb_io_open_descriptor(klass, FIX2INT(descriptor), FIX2INT(mode), path, timeout, &io_encoding); } static VALUE io_spec_rb_io_open_descriptor_without_encoding(VALUE self, VALUE klass, VALUE descriptor, VALUE mode, VALUE path, VALUE timeout) { diff --git a/optional/capi/ext/struct_spec.c b/optional/capi/ext/struct_spec.c index 9c45bd5672..413249e828 100644 --- a/optional/capi/ext/struct_spec.c +++ b/optional/capi/ext/struct_spec.c @@ -28,7 +28,7 @@ static VALUE struct_spec_rb_struct_aset(VALUE self, VALUE st, VALUE key, VALUE v } /* Only allow setting three attributes, should be sufficient for testing. */ -static VALUE struct_spec_struct_define(VALUE self, VALUE name, +static VALUE struct_spec_rb_struct_define(VALUE self, VALUE name, VALUE attr1, VALUE attr2, VALUE attr3) { const char *a1 = StringValuePtr(attr1); @@ -42,7 +42,7 @@ static VALUE struct_spec_struct_define(VALUE self, VALUE name, } /* Only allow setting three attributes, should be sufficient for testing. */ -static VALUE struct_spec_struct_define_under(VALUE self, VALUE outer, +static VALUE struct_spec_rb_struct_define_under(VALUE self, VALUE outer, VALUE name, VALUE attr1, VALUE attr2, VALUE attr3) { const char *nm = StringValuePtr(name); @@ -62,6 +62,23 @@ static VALUE struct_spec_rb_struct_size(VALUE self, VALUE st) { return rb_struct_size(st); } +#if defined(RUBY_VERSION_IS_3_3) +/* Only allow setting three attributes, should be sufficient for testing. */ +static VALUE struct_spec_rb_data_define(VALUE self, VALUE superclass, + VALUE attr1, VALUE attr2, VALUE attr3) { + + const char *a1 = StringValuePtr(attr1); + const char *a2 = StringValuePtr(attr2); + const char *a3 = StringValuePtr(attr3); + + if (superclass == Qnil) { + superclass = 0; + } + + return rb_data_define(superclass, a1, a2, a3, NULL); +} +#endif + void Init_struct_spec(void) { VALUE cls = rb_define_class("CApiStructSpecs", rb_cObject); rb_define_method(cls, "rb_struct_aref", struct_spec_rb_struct_aref, 2); @@ -69,10 +86,13 @@ void Init_struct_spec(void) { rb_define_method(cls, "rb_struct_s_members", struct_spec_rb_struct_s_members, 1); rb_define_method(cls, "rb_struct_members", struct_spec_rb_struct_members, 1); rb_define_method(cls, "rb_struct_aset", struct_spec_rb_struct_aset, 3); - rb_define_method(cls, "rb_struct_define", struct_spec_struct_define, 4); - rb_define_method(cls, "rb_struct_define_under", struct_spec_struct_define_under, 5); + rb_define_method(cls, "rb_struct_define", struct_spec_rb_struct_define, 4); + rb_define_method(cls, "rb_struct_define_under", struct_spec_rb_struct_define_under, 5); rb_define_method(cls, "rb_struct_new", struct_spec_rb_struct_new, 4); rb_define_method(cls, "rb_struct_size", struct_spec_rb_struct_size, 1); +#if defined(RUBY_VERSION_IS_3_3) + rb_define_method(cls, "rb_data_define", struct_spec_rb_data_define, 4); +#endif } #ifdef __cplusplus diff --git a/optional/capi/struct_spec.rb b/optional/capi/struct_spec.rb index 0e9e366908..474c397956 100644 --- a/optional/capi/struct_spec.rb +++ b/optional/capi/struct_spec.rb @@ -209,3 +209,52 @@ end end end + +ruby_version_is "3.3" do + describe "C-API Data function" do + before :each do + @s = CApiStructSpecs.new + end + + describe "rb_data_define" do + it "returns a subclass of Data class when passed nil as the first argument" do + klass = @s.rb_data_define(nil, "a", "b", "c") + + klass.should.is_a? Class + klass.superclass.should == Data + end + + it "returns a subclass of a class when passed as the first argument" do + superclass = Class.new(Data) + klass = @s.rb_data_define(superclass, "a", "b", "c") + + klass.should.is_a? Class + klass.superclass.should == superclass + end + + it "creates readers for the members" do + klass = @s.rb_data_define(nil, "a", "b", "c") + obj = klass.new(1, 2, 3) + + obj.a.should == 1 + obj.b.should == 2 + obj.c.should == 3 + end + + it "returns the member names as Symbols" do + klass = @s.rb_data_define(nil, "a", "b", "c") + obj = klass.new(0, 0, 0) + + obj.members.should == [:a, :b, :c] + end + + it "raises an ArgumentError if arguments contain duplicate member name" do + -> { @s.rb_data_define(nil, "a", "b", "a") }.should raise_error(ArgumentError) + end + + it "raises when first argument is not a class" do + -> { @s.rb_data_define([], "a", "b", "c") }.should raise_error(TypeError, "wrong argument type Array (expected Class)") + end + end + end +end diff --git a/security/cve_2024_49761_spec.rb b/security/cve_2024_49761_spec.rb new file mode 100644 index 0000000000..54ede39032 --- /dev/null +++ b/security/cve_2024_49761_spec.rb @@ -0,0 +1,9 @@ +require_relative '../spec_helper' + +ruby_version_is "3.2" do + describe "CVE-2024-49761 is resisted by" do + it "the Regexp implementation handling that regular expression in linear time" do + Regexp.linear_time?(/�*((?:\d+)|(?:x[a-fA-F0-9]+));/).should == true + end + end +end