diff --git a/spec/ruby/CONTRIBUTING.md b/spec/ruby/CONTRIBUTING.md index 20258e5c36..adfc2fb0ca 100644 --- a/spec/ruby/CONTRIBUTING.md +++ b/spec/ruby/CONTRIBUTING.md @@ -175,9 +175,10 @@ end #### Guard for bug -In case there is a bug in MRI but the expected behavior is obvious. +In case there is a bug in MRI and the fix will be backported to previous versions. +If it is not backported or not likely, use `ruby_version_is` instead. First, file a bug at https://bugs.ruby-lang.org/. -It is better to use a `ruby_version_is` guard if there was a release with the fix. +The problem is `ruby_bug` would make non-MRI implementations fail this spec while MRI itself does not pass it, so it should only be used if the bug is/will be fixed and backported. ```ruby ruby_bug '#13669', ''...'3.2' do diff --git a/spec/ruby/README.md b/spec/ruby/README.md index 55b248a6c3..24b4719fdd 100644 --- a/spec/ruby/README.md +++ b/spec/ruby/README.md @@ -144,10 +144,9 @@ The file `/etc/services` is required for socket specs (package `netbase` on Debi ### Socket specs from rubysl-socket -Most specs under `library/socket` were imported from [the rubysl-socket project](https://github.com/rubysl/rubysl-socket). +Most specs under `library/socket` were imported from the rubysl-socket project (which is no longer on GitHub). The 3 copyright holders of rubysl-socket, Yorick Peterse, Chuck Remes and -Brian Shirai, [agreed to relicense those specs](https://github.com/rubysl/rubysl-socket/issues/15) -under the MIT license in ruby/spec. +Brian Shirai, agreed to relicense those specs under the MIT license in ruby/spec. ### History and RubySpec diff --git a/spec/ruby/core/dir/fixtures/common.rb b/spec/ruby/core/dir/fixtures/common.rb index a8d6e69c44..087f46b331 100644 --- a/spec/ruby/core/dir/fixtures/common.rb +++ b/spec/ruby/core/dir/fixtures/common.rb @@ -82,6 +82,7 @@ module DirSpecs special/test{1}/file[1] special/{}/special + special/test\ +()[]{}/hello_world.erb ] platform_is_not :windows do diff --git a/spec/ruby/core/dir/glob_spec.rb b/spec/ruby/core/dir/glob_spec.rb index 43dac73eee..06b52b90fb 100644 --- a/spec/ruby/core/dir/glob_spec.rb +++ b/spec/ruby/core/dir/glob_spec.rb @@ -79,6 +79,7 @@ describe "Dir.glob" do nested/ nested/.dotsubir/ special/ + special/test\ +()[]{}/ special/test{1}/ special/{}/ subdir_one/ @@ -130,6 +131,7 @@ describe "Dir.glob" do ./nested/ ./nested/.dotsubir/ ./special/ + ./special/test\ +()[]{}/ ./special/test{1}/ ./special/{}/ ./subdir_one/ diff --git a/spec/ruby/core/dir/shared/glob.rb b/spec/ruby/core/dir/shared/glob.rb index 60d4a8c97a..33b2828c27 100644 --- a/spec/ruby/core/dir/shared/glob.rb +++ b/spec/ruby/core/dir/shared/glob.rb @@ -111,6 +111,10 @@ describe :dir_glob, shared: true do it "matches files with backslashes in their name" do Dir.glob('special/\\\\{a,b}').should == ['special/\a'] end + + it "matches directory with special characters in their name in complex patterns" do + Dir.glob("special/test +()\\[\\]\\{\\}/hello_world{.{en},}{.{html},}{+{phone},}{.{erb},}").should == ['special/test +()[]{}/hello_world.erb'] + end end it "matches regexp special ^" do @@ -225,6 +229,7 @@ describe :dir_glob, shared: true do dir/ nested/ special/ + special/test\ +()[]{}/ special/test{1}/ special/{}/ subdir_one/ diff --git a/spec/ruby/core/io/fixtures/classes.rb b/spec/ruby/core/io/fixtures/classes.rb index 067ab59d93..204a2a101b 100644 --- a/spec/ruby/core/io/fixtures/classes.rb +++ b/spec/ruby/core/io/fixtures/classes.rb @@ -7,6 +7,18 @@ module IOSpecs class SubIO < IO end + class SubIOWithRedefinedNew < IO + def self.new(...) + ScratchPad << :redefined_new_called + super + end + + def initialize(...) + ScratchPad << :call_original_initialize + super + end + end + def self.collector Proc.new { |x| ScratchPad << x } end diff --git a/spec/ruby/core/io/pipe_spec.rb b/spec/ruby/core/io/pipe_spec.rb index 2f2cf06f4d..aee0d9003f 100644 --- a/spec/ruby/core/io/pipe_spec.rb +++ b/spec/ruby/core/io/pipe_spec.rb @@ -25,6 +25,17 @@ describe "IO.pipe" do @r.should be_an_instance_of(IOSpecs::SubIO) @w.should be_an_instance_of(IOSpecs::SubIO) end + + it "does not use IO.new method to create pipes and allows its overriding" do + ScratchPad.record [] + + # so redefined .new is not called, but original #initialize is + @r, @w = IOSpecs::SubIOWithRedefinedNew.pipe + ScratchPad.recorded.should == [:call_original_initialize, :call_original_initialize] # called 2 times - for each pipe (r and w) + + @r.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew) + @w.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew) + end end describe "IO.pipe" do diff --git a/spec/ruby/core/kernel/shared/sprintf.rb b/spec/ruby/core/kernel/shared/sprintf.rb index 59f5ab0036..2db50bd686 100644 --- a/spec/ruby/core/kernel/shared/sprintf.rb +++ b/spec/ruby/core/kernel/shared/sprintf.rb @@ -293,13 +293,13 @@ describe :kernel_sprintf, shared: true do it "raises ArgumentError if argument is a string of several characters" do -> { @method.call("%c", "abc") - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, /%c requires a character/) end it "raises ArgumentError if argument is an empty string" do -> { @method.call("%c", "") - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, /%c requires a character/) end end @@ -313,9 +313,56 @@ describe :kernel_sprintf, shared: true do end end - it "supports Unicode characters" do - @method.call("%c", 1286).should == "Ԇ" - @method.call("%c", "ش").should == "ش" + it "raises TypeError if argument is not String or Integer and cannot be converted to them" do + -> { + @method.call("%c", []) + }.should raise_error(TypeError, /no implicit conversion of Array into Integer/) + end + + it "raises TypeError if argument is nil" do + -> { + @method.call("%c", nil) + }.should raise_error(TypeError, /no implicit conversion from nil to integer/) + end + + it "tries to convert argument to String with to_str" do + obj = BasicObject.new + def obj.to_str + "a" + end + + @method.call("%c", obj).should == "a" + end + + it "tries to convert argument to Integer with to_int" do + obj = BasicObject.new + def obj.to_int + 90 + end + + @method.call("%c", obj).should == "Z" + end + + it "raises TypeError if converting to String with to_str returns non-String" do + obj = BasicObject.new + def obj.to_str + :foo + end + + -> { + @method.call("%c", obj) + }.should raise_error(TypeError, /can't convert BasicObject to String/) + end + + it "raises TypeError if converting to Integer with to_int returns non-Integer" do + obj = BasicObject.new + def obj.to_str + :foo + end + + -> { + @method.call("%c", obj) + }.should raise_error(TypeError, /can't convert BasicObject to String/) end end @@ -374,11 +421,11 @@ describe :kernel_sprintf, shared: true do @method.call("%4.6s", "abcdefg").should == "abcdef" end - it "formats nli with width" do + it "formats nil with width" do @method.call("%6s", nil).should == " " end - it "formats nli with precision" do + it "formats nil with precision" do @method.call("%.6s", nil).should == "" end @@ -939,4 +986,8 @@ describe :kernel_sprintf, shared: true do } end end + + it "does not raise error when passed more arguments than needed" do + sprintf("%s %d %c", "string", 2, "c", []).should == "string 2 c" + end end diff --git a/spec/ruby/core/kernel/shared/sprintf_encoding.rb b/spec/ruby/core/kernel/shared/sprintf_encoding.rb index 5ca66b9083..9cedb8b662 100644 --- a/spec/ruby/core/kernel/shared/sprintf_encoding.rb +++ b/spec/ruby/core/kernel/shared/sprintf_encoding.rb @@ -1,3 +1,5 @@ +# Keep encoding-related specs in a separate shared example to be able to skip them in IO/File/StringIO specs. +# It's difficult to check result's encoding in the test after writing to a file/io buffer. describe :kernel_sprintf_encoding, shared: true do it "can produce a string with valid encoding" do string = @method.call("good day %{valid}", valid: "e") @@ -25,7 +27,7 @@ describe :kernel_sprintf_encoding, shared: true do result.encoding.should equal(Encoding::UTF_8) end - it "raises Encoding::CompatibilityError if both encodings are ASCII compatible and there ano not ASCII characters" do + it "raises Encoding::CompatibilityError if both encodings are ASCII compatible and there are not ASCII characters" do string = "Ä %s".encode('windows-1252') argument = "Ђ".encode('windows-1251') @@ -33,4 +35,33 @@ describe :kernel_sprintf_encoding, shared: true do @method.call(string, argument) }.should raise_error(Encoding::CompatibilityError) end + + describe "%c" do + it "supports Unicode characters" do + result = @method.call("%c", 1286) + result.should == "Ԇ" + result.bytes.should == [212, 134] + + result = @method.call("%c", "ش") + result.should == "ش" + result.bytes.should == [216, 180] + end + + it "raises error when a codepoint isn't representable in an encoding of a format string" do + format = "%c".encode("ASCII") + + -> { + @method.call(format, 1286) + }.should raise_error(RangeError, /out of char range/) + end + + it "uses the encoding of the format string to interpret codepoints" do + format = "%c".force_encoding("euc-jp") + result = @method.call(format, 9415601) + + result.encoding.should == Encoding::EUC_JP + result.should == "é".encode(Encoding::EUC_JP) + result.bytes.should == [143, 171, 177] + end + end end diff --git a/spec/ruby/core/regexp/compile_spec.rb b/spec/ruby/core/regexp/compile_spec.rb index 329cb4f753..c41399cfbb 100644 --- a/spec/ruby/core/regexp/compile_spec.rb +++ b/spec/ruby/core/regexp/compile_spec.rb @@ -13,3 +13,7 @@ end describe "Regexp.compile given a Regexp" do it_behaves_like :regexp_new_regexp, :compile end + +describe "Regexp.new given a non-String/Regexp" do + it_behaves_like :regexp_new_non_string_or_regexp, :compile +end diff --git a/spec/ruby/core/regexp/new_spec.rb b/spec/ruby/core/regexp/new_spec.rb index ce662b7a4f..65f612df55 100644 --- a/spec/ruby/core/regexp/new_spec.rb +++ b/spec/ruby/core/regexp/new_spec.rb @@ -11,17 +11,9 @@ end describe "Regexp.new given a Regexp" do it_behaves_like :regexp_new_regexp, :new - it_behaves_like :regexp_new_string_binary, :compile + it_behaves_like :regexp_new_string_binary, :new end -describe "Regexp.new given an Integer" do - it "raises a TypeError" do - -> { Regexp.new(1) }.should raise_error(TypeError) - end -end - -describe "Regexp.new given a Float" do - it "raises a TypeError" do - -> { Regexp.new(1.0) }.should raise_error(TypeError) - end +describe "Regexp.new given a non-String/Regexp" do + it_behaves_like :regexp_new_non_string_or_regexp, :new end diff --git a/spec/ruby/core/regexp/shared/new.rb b/spec/ruby/core/regexp/shared/new.rb index a6d9c48112..10c2d3d390 100644 --- a/spec/ruby/core/regexp/shared/new.rb +++ b/spec/ruby/core/regexp/shared/new.rb @@ -24,6 +24,32 @@ describe :regexp_new, shared: true do end end +describe :regexp_new_non_string_or_regexp, shared: true do + it "calls #to_str method for non-String/Regexp argument" do + obj = Object.new + def obj.to_str() "a" end + + Regexp.send(@method, obj).should == /a/ + end + + it "raises TypeError if there is no #to_str method for non-String/Regexp argument" do + obj = Object.new + -> { Regexp.send(@method, obj) }.should raise_error(TypeError, "no implicit conversion of Object into String") + + -> { Regexp.send(@method, 1) }.should raise_error(TypeError, "no implicit conversion of Integer into String") + -> { Regexp.send(@method, 1.0) }.should raise_error(TypeError, "no implicit conversion of Float into String") + -> { Regexp.send(@method, :symbol) }.should raise_error(TypeError, "no implicit conversion of Symbol into String") + -> { Regexp.send(@method, []) }.should raise_error(TypeError, "no implicit conversion of Array into String") + end + + it "raises TypeError if #to_str returns non-String value" do + obj = Object.new + def obj.to_str() [] end + + -> { Regexp.send(@method, obj) }.should raise_error(TypeError, /can't convert Object to String/) + end +end + describe :regexp_new_string, shared: true do it "uses the String argument as an unescaped literal to construct a Regexp object" do Regexp.send(@method, "^hi{2,3}fo.o$").should == /^hi{2,3}fo.o$/ @@ -97,6 +123,16 @@ describe :regexp_new_string, shared: true do (r.options & Regexp::EXTENDED).should_not == 0 end + it "does not try to convert the second argument to Integer with #to_int method call" do + ScratchPad.clear + obj = Object.new + def obj.to_int() ScratchPad.record(:called) end + + Regexp.send(@method, "Hi", obj) + + ScratchPad.recorded.should == nil + end + ruby_version_is ""..."3.2" do it "treats any non-Integer, non-nil, non-false second argument as IGNORECASE" do r = Regexp.send(@method, 'Hi', Object.new) diff --git a/spec/ruby/core/time/shared/local.rb b/spec/ruby/core/time/shared/local.rb index 997b7186f1..2dba23dbd7 100644 --- a/spec/ruby/core/time/shared/local.rb +++ b/spec/ruby/core/time/shared/local.rb @@ -9,10 +9,10 @@ describe :time_local, shared: true do =begin platform_is_not :windows do describe "timezone changes" do - it "correctly adjusts the timezone change to 'CEST' on 'Europe/Amsterdam'" do + it "correctly adjusts the timezone change to 'CET' on 'Europe/Amsterdam'" do with_timezone("Europe/Amsterdam") do - Time.send(@method, 1940, 5, 16).to_a.should == - [0, 40, 1, 16, 5, 1940, 4, 137, true, "CEST"] + Time.send(@method, 1970, 5, 16).to_a.should == + [0, 0, 0, 16, 5, 1970, 6, 136, false, "CET"] end end end diff --git a/spec/ruby/language/block_spec.rb b/spec/ruby/language/block_spec.rb index 42652152a1..d918c12beb 100644 --- a/spec/ruby/language/block_spec.rb +++ b/spec/ruby/language/block_spec.rb @@ -983,3 +983,77 @@ describe "Post-args" do end end end + +describe "Anonymous block forwarding" do + ruby_version_is "3.1" do + it "forwards blocks to other functions that formally declare anonymous blocks" do + eval <<-EOF + def b(&); c(&) end + def c(&); yield :non_null end + EOF + + b { |c| c }.should == :non_null + end + + it "requires the anonymous block parameter to be declared if directly passing a block" do + -> { eval "def a; b(&); end; def b; end" }.should raise_error(SyntaxError) + end + + it "works when it's the only declared parameter" do + eval <<-EOF + def inner; yield end + def block_only(&); inner(&) end + EOF + + block_only { 1 }.should == 1 + end + + it "works alongside positional parameters" do + eval <<-EOF + def inner; yield end + def pos(arg1, &); inner(&) end + EOF + + pos(:a) { 1 }.should == 1 + end + + it "works alongside positional arguments and splatted keyword arguments" do + eval <<-EOF + def inner; yield end + def pos_kwrest(arg1, **kw, &); inner(&) end + EOF + + pos_kwrest(:a, arg: 3) { 1 }.should == 1 + end + + it "works alongside positional arguments and disallowed keyword arguments" do + eval <<-EOF + def inner; yield end + def no_kw(arg1, **nil, &); inner(&) end + EOF + + no_kw(:a) { 1 }.should == 1 + end + end + + ruby_version_is "3.2" do + it "works alongside explicit keyword arguments" do + eval <<-EOF + def inner; yield end + def rest_kw(*a, kwarg: 1, &); inner(&) end + def kw(kwarg: 1, &); inner(&) end + def pos_kw_kwrest(arg1, kwarg: 1, **kw, &); inner(&) end + def pos_rkw(arg1, kwarg1:, &); inner(&) end + def all(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, &); inner(&) end + def all_kwrest(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, **kw, &); inner(&) end + EOF + + rest_kw { 1 }.should == 1 + kw { 1 }.should == 1 + pos_kw_kwrest(:a) { 1 }.should == 1 + pos_rkw(:a, kwarg1: 3) { 1 }.should == 1 + all(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1 + all_kwrest(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1 + end + end +end diff --git a/spec/ruby/language/keyword_arguments_spec.rb b/spec/ruby/language/keyword_arguments_spec.rb index 0c72f59d38..8771c5806c 100644 --- a/spec/ruby/language/keyword_arguments_spec.rb +++ b/spec/ruby/language/keyword_arguments_spec.rb @@ -58,6 +58,24 @@ ruby_version_is "3.0" do m(kw: 1).should == [] -> { m(kw: 1, kw2: 2) }.should raise_error(ArgumentError, 'unknown keyword: :kw2') -> { m(kw: 1, true => false) }.should raise_error(ArgumentError, 'unknown keyword: true') + -> { m(kw: 1, a: 1, b: 2, c: 3) }.should raise_error(ArgumentError, 'unknown keywords: :a, :b, :c') + end + + it "raises ArgumentError exception when required keyword argument is not passed" do + def m(a:, b:, c:) + [a, b, c] + end + + -> { m(a: 1, b: 2) }.should raise_error(ArgumentError, /missing keyword: :c/) + -> { m() }.should raise_error(ArgumentError, /missing keywords: :a, :b, :c/) + end + + it "raises ArgumentError for missing keyword arguments even if there are extra ones" do + def m(a:) + a + end + + -> { m(b: 1) }.should raise_error(ArgumentError, /missing keyword: :a/) end it "handle * and ** at the same call site" do diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb index d464e79403..acca074974 100644 --- a/spec/ruby/language/method_spec.rb +++ b/spec/ruby/language/method_spec.rb @@ -571,6 +571,13 @@ describe "A method" do end end + evaluate <<-ruby do + def m(a:, **kw) [a, kw] end + ruby + + -> { m(b: 1) }.should raise_error(ArgumentError) + end + evaluate <<-ruby do def m(a: 1) a end ruby diff --git a/spec/ruby/language/proc_spec.rb b/spec/ruby/language/proc_spec.rb index 8360967ec8..f8a29962b0 100644 --- a/spec/ruby/language/proc_spec.rb +++ b/spec/ruby/language/proc_spec.rb @@ -237,4 +237,11 @@ describe "A Proc" do end end end + + describe "taking |required keyword arguments, **kw| arguments" do + it "raises ArgumentError for missing required argument" do + p = proc { |a:, **kw| [a, kw] } + -> { p.call() }.should raise_error(ArgumentError) + end + end end diff --git a/spec/ruby/library/bigdecimal/to_r_spec.rb b/spec/ruby/library/bigdecimal/to_r_spec.rb index 91d2b33993..c350beff08 100644 --- a/spec/ruby/library/bigdecimal/to_r_spec.rb +++ b/spec/ruby/library/bigdecimal/to_r_spec.rb @@ -13,4 +13,16 @@ describe "BigDecimal#to_r" do r.denominator.should eql(1000000000000000000000000) end + it "returns a Rational from a BigDecimal with an exponent" do + r = BigDecimal("1E2").to_r + r.numerator.should eql(100) + r.denominator.should eql(1) + end + + it "returns a Rational from a negative BigDecimal with an exponent" do + r = BigDecimal("-1E2").to_r + r.numerator.should eql(-100) + r.denominator.should eql(1) + end + end diff --git a/spec/ruby/library/stringio/open_spec.rb b/spec/ruby/library/stringio/open_spec.rb index acab6e9056..3068e19435 100644 --- a/spec/ruby/library/stringio/open_spec.rb +++ b/spec/ruby/library/stringio/open_spec.rb @@ -167,10 +167,14 @@ describe "StringIO.open when passed [Object]" do io.should equal(ret) end - it "sets the mode to read-write" do + it "sets the mode to read-write (r+)" do io = StringIO.open("example") io.closed_read?.should be_false io.closed_write?.should be_false + + io = StringIO.new("example") + io.printf("%d", 123) + io.string.should == "123mple" end it "tries to convert the passed Object to a String using #to_str" do @@ -195,10 +199,14 @@ describe "StringIO.open when passed no arguments" do io.should equal(ret) end - it "sets the mode to read-write" do + it "sets the mode to read-write (r+)" do io = StringIO.open io.closed_read?.should be_false io.closed_write?.should be_false + + io = StringIO.new("example") + io.printf("%d", 123) + io.string.should == "123mple" end it "uses an empty String as the StringIO backend" do diff --git a/spec/ruby/library/stringio/printf_spec.rb b/spec/ruby/library/stringio/printf_spec.rb index 9dd1a3b410..f3f669a185 100644 --- a/spec/ruby/library/stringio/printf_spec.rb +++ b/spec/ruby/library/stringio/printf_spec.rb @@ -4,7 +4,7 @@ require_relative '../../core/kernel/shared/sprintf' describe "StringIO#printf" do before :each do - @io = StringIO.new('example') + @io = StringIO.new() end it "returns nil" do @@ -12,9 +12,9 @@ describe "StringIO#printf" do end it "pads self with \\000 when the current position is after the end" do - @io.pos = 10 + @io.pos = 3 @io.printf("%d", 123) - @io.string.should == "example\000\000\000123" + @io.string.should == "\000\000\000123" end it "performs format conversion" do @@ -39,6 +39,27 @@ describe "StringIO#printf" do end end +describe "StringIO#printf when in read-write mode" do + before :each do + @io = StringIO.new("example", "r+") + end + + it "starts from the beginning" do + @io.printf("%s", "abcdefghijk") + @io.string.should == "abcdefghijk" + end + + it "does not truncate existing string" do + @io.printf("%s", "abc") + @io.string.should == "abcmple" + end + + it "correctly updates self's position" do + @io.printf("%s", "abc") + @io.pos.should eql(3) + end +end + describe "StringIO#printf when in append mode" do before :each do @io = StringIO.new("example", "a") diff --git a/spec/ruby/library/stringio/read_nonblock_spec.rb b/spec/ruby/library/stringio/read_nonblock_spec.rb index 2a8f926bd0..d4ec56d9aa 100644 --- a/spec/ruby/library/stringio/read_nonblock_spec.rb +++ b/spec/ruby/library/stringio/read_nonblock_spec.rb @@ -5,10 +5,21 @@ require_relative 'shared/sysread' describe "StringIO#read_nonblock when passed length, buffer" do it_behaves_like :stringio_read, :read_nonblock + + it "accepts :exception option" do + io = StringIO.new("example") + io.read_nonblock(3, buffer = "", exception: true) + buffer.should == "exa" + end end describe "StringIO#read_nonblock when passed length" do it_behaves_like :stringio_read_length, :read_nonblock + + it "accepts :exception option" do + io = StringIO.new("example") + io.read_nonblock(3, exception: true).should == "exa" + end end describe "StringIO#read_nonblock when passed nil" do diff --git a/spec/ruby/library/stringio/write_nonblock_spec.rb b/spec/ruby/library/stringio/write_nonblock_spec.rb index 4f4c5039fe..a457b97667 100644 --- a/spec/ruby/library/stringio/write_nonblock_spec.rb +++ b/spec/ruby/library/stringio/write_nonblock_spec.rb @@ -8,6 +8,12 @@ end describe "StringIO#write_nonblock when passed [String]" do it_behaves_like :stringio_write_string, :write_nonblock + + it "accepts :exception option" do + io = StringIO.new("12345", "a") + io.write_nonblock("67890", exception: true) + io.string.should == "1234567890" + end end describe "StringIO#write_nonblock when self is not writable" do diff --git a/spec/ruby/library/zlib/deflate/deflate_spec.rb b/spec/ruby/library/zlib/deflate/deflate_spec.rb index 828880f8d8..50a563ef6f 100644 --- a/spec/ruby/library/zlib/deflate/deflate_spec.rb +++ b/spec/ruby/library/zlib/deflate/deflate_spec.rb @@ -58,6 +58,11 @@ describe "Zlib::Deflate#deflate" do Array.new(31, 0) + [24, 128, 0, 0, 1]).pack('C*') end + + it "has a binary encoding" do + @deflator.deflate("").encoding.should == Encoding::BINARY + @deflator.finish.encoding.should == Encoding::BINARY + end end describe "Zlib::Deflate#deflate" do diff --git a/spec/ruby/library/zlib/inflate/inflate_spec.rb b/spec/ruby/library/zlib/inflate/inflate_spec.rb index cc33bd4c32..79b72bf91c 100644 --- a/spec/ruby/library/zlib/inflate/inflate_spec.rb +++ b/spec/ruby/library/zlib/inflate/inflate_spec.rb @@ -39,6 +39,13 @@ describe "Zlib::Inflate#inflate" do @inflator.finish.should == 'uncompressed_data' end + it "has a binary encoding" do + data = [120, 156, 99, 96, 128, 1, 0, 0, 10, 0, 1].pack('C*') + unzipped = @inflator.inflate data + @inflator.finish.encoding.should == Encoding::BINARY + unzipped.encoding.should == Encoding::BINARY + end + end describe "Zlib::Inflate.inflate" do diff --git a/spec/ruby/optional/capi/encoding_spec.rb b/spec/ruby/optional/capi/encoding_spec.rb index ae557b03d7..aa632b963b 100644 --- a/spec/ruby/optional/capi/encoding_spec.rb +++ b/spec/ruby/optional/capi/encoding_spec.rb @@ -63,6 +63,48 @@ describe "C-API Encoding function" do end end + describe "rb_enc_strlen" do + before :each do + @str = 'こにちわ' # Each codepoint in this string is 3 bytes in UTF-8 + end + + it "returns the correct string length for the encoding" do + @s.rb_enc_strlen(@str, @str.bytesize, Encoding::UTF_8).should == 4 + @s.rb_enc_strlen(@str, @str.bytesize, Encoding::BINARY).should == 12 + end + + it "returns the string length based on a fixed-width encoding's character length, even if the encoding is incompatible" do + @s.rb_enc_strlen(@str, @str.bytesize, Encoding::UTF_16BE).should == 6 + @s.rb_enc_strlen(@str, @str.bytesize, Encoding::UTF_16LE).should == 6 + @s.rb_enc_strlen(@str, @str.bytesize, Encoding::UTF_32BE).should == 3 + @s.rb_enc_strlen(@str, @str.bytesize, Encoding::UTF_32LE).should == 3 + end + + it "does not consider strings to be NUL-terminated" do + s = "abc\0def" + @s.rb_enc_strlen(s, s.bytesize, Encoding::US_ASCII).should == 7 + @s.rb_enc_strlen(s, s.bytesize, Encoding::UTF_8).should == 7 + end + + describe "handles broken strings" do + it "combines valid character and invalid character counts in UTF-8" do + # The result is 3 because `rb_enc_strlen` counts the first valid character and then adds + # the byte count for the invalid character that follows for 1 + 2. + @s.rb_enc_strlen(@str, 5, Encoding::UTF_8).should == 3 + end + + it "combines valid character and invalid character counts in UTF-16" do + @s.rb_enc_strlen(@str, 5, Encoding::UTF_16BE).should == 3 + end + + it "rounds up for fixed-width encodings" do + @s.rb_enc_strlen(@str, 7, Encoding::UTF_32BE).should == 2 + @s.rb_enc_strlen(@str, 7, Encoding::UTF_32LE).should == 2 + @s.rb_enc_strlen(@str, 5, Encoding::BINARY).should == 5 + end + end + end + describe "rb_enc_find" do it "returns the encoding of an Encoding" do @s.rb_enc_find("UTF-8").should == "UTF-8" diff --git a/spec/ruby/optional/capi/ext/encoding_spec.c b/spec/ruby/optional/capi/ext/encoding_spec.c index c49f6cde7e..865fc484be 100644 --- a/spec/ruby/optional/capi/ext/encoding_spec.c +++ b/spec/ruby/optional/capi/ext/encoding_spec.c @@ -301,6 +301,14 @@ static VALUE encoding_spec_rb_enc_codelen(VALUE self, VALUE code, VALUE encoding return INT2FIX(rb_enc_codelen(c, enc)); } +static VALUE encoding_spec_rb_enc_strlen(VALUE self, VALUE str, VALUE length, VALUE encoding) { + int l = FIX2INT(length); + char *p = RSTRING_PTR(str); + char *e = p + l; + + return LONG2FIX(rb_enc_strlen(p, e, rb_to_encoding(encoding))); +} + void Init_encoding_spec(void) { VALUE cls; native_rb_encoding_pointer = (rb_encoding**) malloc(sizeof(rb_encoding*)); @@ -335,6 +343,7 @@ void Init_encoding_spec(void) { rb_define_method(cls, "rb_enc_compatible", encoding_spec_rb_enc_compatible, 2); rb_define_method(cls, "rb_enc_copy", encoding_spec_rb_enc_copy, 2); rb_define_method(cls, "rb_enc_codelen", encoding_spec_rb_enc_codelen, 2); + rb_define_method(cls, "rb_enc_strlen", encoding_spec_rb_enc_strlen, 3); rb_define_method(cls, "rb_enc_find", encoding_spec_rb_enc_find, 1); rb_define_method(cls, "rb_enc_find_index", encoding_spec_rb_enc_find_index, 1); rb_define_method(cls, "rb_enc_isalnum", encoding_spec_rb_enc_isalnum, 2); diff --git a/spec/ruby/optional/capi/ext/string_spec.c b/spec/ruby/optional/capi/ext/string_spec.c index b9a4a55853..9cbb50484d 100644 --- a/spec/ruby/optional/capi/ext/string_spec.c +++ b/spec/ruby/optional/capi/ext/string_spec.c @@ -437,6 +437,12 @@ VALUE string_spec_RSTRING_PTR_read(VALUE self, VALUE str, VALUE path) { return capacities; } +VALUE string_spec_RSTRING_PTR_null_terminate(VALUE self, VALUE str, VALUE min_length) { + char* ptr = RSTRING_PTR(str); + char* end = ptr + RSTRING_LEN(str); + return rb_str_new(end, FIX2LONG(min_length)); +} + VALUE string_spec_StringValue(VALUE self, VALUE str) { return StringValue(str); } @@ -662,6 +668,7 @@ void Init_string_spec(void) { rb_define_method(cls, "RSTRING_PTR_after_funcall", string_spec_RSTRING_PTR_after_funcall, 2); rb_define_method(cls, "RSTRING_PTR_after_yield", string_spec_RSTRING_PTR_after_yield, 1); rb_define_method(cls, "RSTRING_PTR_read", string_spec_RSTRING_PTR_read, 2); + rb_define_method(cls, "RSTRING_PTR_null_terminate", string_spec_RSTRING_PTR_null_terminate, 2); rb_define_method(cls, "StringValue", string_spec_StringValue, 1); rb_define_method(cls, "SafeStringValue", string_spec_SafeStringValue, 1); rb_define_method(cls, "rb_str_hash", string_spec_rb_str_hash, 1); diff --git a/spec/ruby/optional/capi/ext/util_spec.c b/spec/ruby/optional/capi/ext/util_spec.c index a7269353c2..95ba71ea9d 100644 --- a/spec/ruby/optional/capi/ext/util_spec.c +++ b/spec/ruby/optional/capi/ext/util_spec.c @@ -7,15 +7,18 @@ extern "C" { #endif VALUE util_spec_rb_scan_args(VALUE self, VALUE argv, VALUE fmt, VALUE expected, VALUE acc) { - int i, result, argc = (int)RARRAY_LEN(argv); - VALUE args[6], failed, a1, a2, a3, a4, a5, a6; + int result, argc; + VALUE a1, a2, a3, a4, a5, a6; - failed = rb_intern("failed"); - a1 = a2 = a3 = a4 = a5 = a6 = failed; + argc = (int) RARRAY_LEN(argv); + VALUE* args = RARRAY_PTR(argv); + /* the line above can be replaced with this for Ruby implementations which do not support RARRAY_PTR() yet + VALUE args[6]; + for(int i = 0; i < argc; i++) { + args[i] = rb_ary_entry(argv, i); + } */ - for(i = 0; i < argc; i++) { - args[i] = rb_ary_entry(argv, i); - } + a1 = a2 = a3 = a4 = a5 = a6 = INT2FIX(-1); #ifdef RB_SCAN_ARGS_KEYWORDS if (*RSTRING_PTR(fmt) == 'k') { diff --git a/spec/ruby/optional/capi/string_spec.rb b/spec/ruby/optional/capi/string_spec.rb index 7ad4d10ee4..0558fc9f7d 100644 --- a/spec/ruby/optional/capi/string_spec.rb +++ b/spec/ruby/optional/capi/string_spec.rb @@ -97,6 +97,32 @@ describe "C-API String function" do end end + describe "rb_str_set_len on a UTF-16 String" do + before :each do + @str = "abcdefghij".force_encoding(Encoding::UTF_16BE) + # Make sure to unshare the string + @s.rb_str_modify(@str) + end + + it "inserts two NULL bytes at the length" do + @s.rb_str_set_len(@str, 4).b.should == "abcd".b + @s.rb_str_set_len(@str, 8).b.should == "abcd\x00\x00gh".b + end + end + + describe "rb_str_set_len on a UTF-32 String" do + before :each do + @str = "abcdefghijkl".force_encoding(Encoding::UTF_32BE) + # Make sure to unshare the string + @s.rb_str_modify(@str) + end + + it "inserts four NULL bytes at the length" do + @s.rb_str_set_len(@str, 4).b.should == "abcd".b + @s.rb_str_set_len(@str, 12).b.should == "abcd\x00\x00\x00\x00ijkl".b + end + end + describe "rb_str_buf_new" do it "returns the equivalent of an empty string" do buf = @s.rb_str_buf_new(10, nil) @@ -592,6 +618,12 @@ describe "C-API String function" do capacities[0].should < capacities[1] str.should == "fixture file contents to test read() with RSTRING_PTR" end + + it "terminates the string with at least (encoding min length) \\0 bytes" do + @s.RSTRING_PTR_null_terminate("abc", 1).should == "\x00" + @s.RSTRING_PTR_null_terminate("abc".encode("UTF-16BE"), 2).should == "\x00\x00" + @s.RSTRING_PTR_null_terminate("abc".encode("UTF-32BE"), 4).should == "\x00\x00\x00\x00" + end end describe "RSTRING_LEN" do diff --git a/spec/ruby/optional/capi/util_spec.rb b/spec/ruby/optional/capi/util_spec.rb index 64b0894087..38f6f47b1a 100644 --- a/spec/ruby/optional/capi/util_spec.rb +++ b/spec/ruby/optional/capi/util_spec.rb @@ -15,8 +15,9 @@ describe "C-API Util function" do end it "assigns the required arguments scanned" do - @o.rb_scan_args([1, 2], "2", 2, @acc).should == 2 - ScratchPad.recorded.should == [1, 2] + obj = Object.new + @o.rb_scan_args([obj, 2], "2", 2, @acc).should == 2 + ScratchPad.recorded.should == [obj, 2] end it "raises an ArgumentError if there are insufficient arguments" do diff --git a/spec/ruby/security/cve_2019_8325_spec.rb b/spec/ruby/security/cve_2019_8325_spec.rb index 7c5e216568..bbddb3a6ce 100644 --- a/spec/ruby/security/cve_2019_8325_spec.rb +++ b/spec/ruby/security/cve_2019_8325_spec.rb @@ -5,16 +5,17 @@ require 'rubygems/command_manager' describe "CVE-2019-8325 is resisted by" do describe "sanitising error message components" do - silent_ui = Module.new do - attr_accessor :ui - def self.extended(obj) - obj.ui = Gem::SilentUI.new - end + before :each do + @ui = Gem::SilentUI.new + end + + after :each do + @ui.close end it "for the 'while executing' message" do manager = Gem::CommandManager.new - manager.extend(silent_ui) + manager.ui = @ui def manager.process_args(args, build_args) raise StandardError, "\e]2;nyan\a" end @@ -34,7 +35,7 @@ describe "CVE-2019-8325 is resisted by" do it "for the 'loading command' message" do manager = Gem::CommandManager.new - manager.extend(silent_ui) + manager.ui = @ui def manager.require(x) raise 'foo' end diff --git a/spec/ruby/shared/sizedqueue/new.rb b/spec/ruby/shared/sizedqueue/new.rb index 713785fb50..2573194efb 100644 --- a/spec/ruby/shared/sizedqueue/new.rb +++ b/spec/ruby/shared/sizedqueue/new.rb @@ -1,7 +1,12 @@ describe :sizedqueue_new, shared: true do - it "raises a TypeError when the given argument is not Numeric" do - -> { @object.call("foo") }.should raise_error(TypeError) + it "raises a TypeError when the given argument doesn't respond to #to_int" do + -> { @object.call("12") }.should raise_error(TypeError) -> { @object.call(Object.new) }.should raise_error(TypeError) + + @object.call(12.9).max.should == 12 + object = Object.new + object.define_singleton_method(:to_int) { 42 } + @object.call(object).max.should == 42 end it "raises an argument error when no argument is given" do