From 8db4f25bf4327f169902afd9ea8f4b03b65656f0 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 28 Mar 2022 17:47:04 +0200 Subject: [PATCH] Update to ruby/spec@aaf998f --- spec/ruby/.mspec.constants | 2 + spec/ruby/README.md | 8 +- spec/ruby/core/array/pack/x_spec.rb | 1 + spec/ruby/core/array/sample_spec.rb | 8 + spec/ruby/core/dir/foreach_spec.rb | 7 + spec/ruby/core/enumerable/shared/inject.rb | 7 + .../core/enumerator/lazy/with_index_spec.rb | 8 + spec/ruby/core/exception/full_message_spec.rb | 40 ++- spec/ruby/core/exception/system_exit_spec.rb | 42 +++ spec/ruby/core/file/utime_spec.rb | 4 + .../core/hash/ruby2_keywords_hash_spec.rb | 14 + spec/ruby/core/hash/shared/to_s.rb | 5 +- spec/ruby/core/io/shared/readlines.rb | 5 + .../kernel/define_singleton_method_spec.rb | 15 + spec/ruby/core/math/sqrt_spec.rb | 4 + spec/ruby/core/module/const_get_spec.rb | 14 + spec/ruby/core/module/ruby2_keywords_spec.rb | 79 ++++- spec/ruby/core/proc/parameters_spec.rb | 2 +- spec/ruby/core/random/rand_spec.rb | 5 + spec/ruby/core/string/append_spec.rb | 5 + spec/ruby/core/string/comparison_spec.rb | 4 + spec/ruby/core/string/lstrip_spec.rb | 4 +- spec/ruby/core/string/rindex_spec.rb | 6 +- spec/ruby/core/string/shared/eql.rb | 4 + spec/ruby/core/string/split_spec.rb | 7 + spec/ruby/core/string/strip_spec.rb | 4 +- spec/ruby/core/struct/new_spec.rb | 17 +- spec/ruby/core/thread/shared/exit.rb | 22 +- spec/ruby/fixtures/constants.rb | 10 + spec/ruby/language/keyword_arguments_spec.rb | 307 ++++++++++++++++++ spec/ruby/library/logger/device/close_spec.rb | 4 +- spec/ruby/library/logger/device/write_spec.rb | 4 +- spec/ruby/optional/capi/class_spec.rb | 24 +- spec/ruby/optional/capi/ext/class_spec.c | 24 +- spec/ruby/optional/capi/ext/kernel_spec.c | 27 +- spec/ruby/optional/capi/ext/symbol_spec.c | 11 + spec/ruby/optional/capi/fixtures/class.rb | 10 + spec/ruby/optional/capi/kernel_spec.rb | 65 +++- spec/ruby/optional/capi/symbol_spec.rb | 8 + spec/ruby/shared/kernel/raise.rb | 34 +- spec/ruby/shared/process/exit.rb | 6 + 41 files changed, 821 insertions(+), 56 deletions(-) create mode 100644 spec/ruby/language/keyword_arguments_spec.rb diff --git a/spec/ruby/.mspec.constants b/spec/ruby/.mspec.constants index c070e35496..5dd477eb66 100644 --- a/spec/ruby/.mspec.constants +++ b/spec/ruby/.mspec.constants @@ -39,6 +39,8 @@ CodingUS_ASCII CodingUTF_8 ComparisonTest ConstantSpecsIncludedModule +ConstantSpecsTwo +ConstantSpecsThree ConstantVisibility Coverage CoverageSpecs diff --git a/spec/ruby/README.md b/spec/ruby/README.md index e62ddc7dce..df98723c9b 100644 --- a/spec/ruby/README.md +++ b/spec/ruby/README.md @@ -18,13 +18,13 @@ Every example code has a textual description, which presents several advantages: The specs are written with syntax similar to RSpec 2. They are run with MSpec, the purpose-built framework for running the Ruby Spec Suite. -For more information, see the [MSpec](http://github.com/ruby/mspec) project. +For more information, see the [MSpec](https://github.com/ruby/mspec) project. The specs describe the [language syntax](language/), the [core library](core/), the [standard library](library/), the [C API for extensions](optional/capi) and the [command line flags](command_line/). The language specs are grouped by keyword while the core and standard library specs are grouped by class and method. ruby/spec is known to be tested in these implementations for every commit: -* [MRI](http://rubyci.org/) on 30 platforms and 4 versions +* [MRI](https://rubyci.org/) on 30 platforms and 4 versions * [JRuby](https://github.com/jruby/jruby/tree/master/spec/ruby) for both 1.7 and 9.x * [TruffleRuby](https://github.com/oracle/truffleruby/tree/master/spec/ruby) * [Opal](https://github.com/opal/opal/tree/master/spec) @@ -70,7 +70,7 @@ Then move to it: $ cd spec -Clone [MSpec](http://github.com/ruby/mspec): +Clone [MSpec](https://github.com/ruby/mspec): $ git clone https://github.com/ruby/mspec.git ../mspec @@ -152,5 +152,5 @@ This project was originally born from [Rubinius](https://github.com/rubinius/rub The revision history of these specs is available [here](https://github.com/ruby/spec/blob/2b886623/CHANGES.before-2008-05-10). These specs were later extracted to their own project, RubySpec, with a specific vision and principles. At the end of 2014, Brian Shirai, the creator of RubySpec, decided to [end RubySpec](http://rubinius.com/2014/12/31/matz-s-ruby-developers-don-t-use-rubyspec/). -A couple months later, the different repositories were merged and [the project was revived](http://eregon.github.io/rubyspec/2015/07/29/rubyspec-is-reborn.html). +A couple months later, the different repositories were merged and [the project was revived](https://eregon.github.io/rubyspec/2015/07/29/rubyspec-is-reborn.html). On 12 January 2016, the name was changed to "The Ruby Spec Suite" for clarity and to let the RubySpec ideology rest in peace. diff --git a/spec/ruby/core/array/pack/x_spec.rb b/spec/ruby/core/array/pack/x_spec.rb index a28dd0bf21..86c3ad1aa4 100644 --- a/spec/ruby/core/array/pack/x_spec.rb +++ b/spec/ruby/core/array/pack/x_spec.rb @@ -30,6 +30,7 @@ describe "Array#pack with format 'x'" do it "does not add a NULL byte when passed the '*' modifier" do [].pack("x*").should == "" + [1, 2].pack("Cx*C").should == "\x01\x02" end end diff --git a/spec/ruby/core/array/sample_spec.rb b/spec/ruby/core/array/sample_spec.rb index 565d7ff7f9..755b46f126 100644 --- a/spec/ruby/core/array/sample_spec.rb +++ b/spec/ruby/core/array/sample_spec.rb @@ -19,10 +19,18 @@ describe "Array#sample" do [].sample.should be_nil end + it "returns nil for an empty array when called without n and a Random is given" do + [].sample(random: Random.new(42)).should be_nil + end + it "returns a single value when not passed a count" do [4].sample.should equal(4) end + it "returns a single value when not passed a count and a Random is given" do + [4].sample(random: Random.new(42)).should equal(4) + end + it "returns an empty Array when passed zero" do [4].sample(0).should == [] end diff --git a/spec/ruby/core/dir/foreach_spec.rb b/spec/ruby/core/dir/foreach_spec.rb index c3ddb27a84..85e8c61fd6 100644 --- a/spec/ruby/core/dir/foreach_spec.rb +++ b/spec/ruby/core/dir/foreach_spec.rb @@ -42,6 +42,13 @@ describe "Dir.foreach" do it "accepts an encoding keyword for the encoding of the entries" do dirs = Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").to_a.sort dirs.each {|dir| dir.encoding.should == Encoding::UTF_8} + + dirs = Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: Encoding::UTF_16LE).to_a.sort + dirs.each {|dir| dir.encoding.should == Encoding::UTF_16LE} + + Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: Encoding::UTF_16LE) do |f| + f.encoding.should == Encoding::UTF_16LE + end end ruby_version_is ""..."2.7" do diff --git a/spec/ruby/core/enumerable/shared/inject.rb b/spec/ruby/core/enumerable/shared/inject.rb index 12e0665dda..e59b82b02e 100644 --- a/spec/ruby/core/enumerable/shared/inject.rb +++ b/spec/ruby/core/enumerable/shared/inject.rb @@ -66,4 +66,11 @@ describe :enumerable_inject, shared: true do it "returns nil when fails(legacy rubycon)" do EnumerableSpecs::EachDefiner.new().send(@method) {|acc,x| 999 }.should == nil end + + ruby_bug '#18635', ''...'3.2' do + it "raises an ArgumentError when no parameters or block is given" do + -> { [1,2].send(@method) }.should raise_error(ArgumentError) + -> { {one: 1, two: 2}.send(@method) }.should raise_error(ArgumentError) + end + end end diff --git a/spec/ruby/core/enumerator/lazy/with_index_spec.rb b/spec/ruby/core/enumerator/lazy/with_index_spec.rb index 3a59ba4116..b6dbe554d9 100644 --- a/spec/ruby/core/enumerator/lazy/with_index_spec.rb +++ b/spec/ruby/core/enumerator/lazy/with_index_spec.rb @@ -26,5 +26,13 @@ ruby_version_is "2.7" do (0..Float::INFINITY).lazy.with_index { |i, idx| result << [i * 2, idx] }.first(3) result.should == [[0,0],[2,1],[4,2]] end + + it "resets after a new call to each" do + enum = (0..2).lazy.with_index.map { |i, idx| [i, idx] } + result = [] + enum.each { |i, idx| result << [i, idx] } + enum.each { |i, idx| result << [i, idx] } + result.should == [[0,0], [1,1], [2,2], [0,0], [1,1], [2,2]] + end end end diff --git a/spec/ruby/core/exception/full_message_spec.rb b/spec/ruby/core/exception/full_message_spec.rb index 4cece9ebf9..9757a2f407 100644 --- a/spec/ruby/core/exception/full_message_spec.rb +++ b/spec/ruby/core/exception/full_message_spec.rb @@ -15,13 +15,19 @@ describe "Exception#full_message" do it "supports :highlight option and adds escape sequences to highlight some strings" do e = RuntimeError.new("Some runtime error") - full_message = e.full_message(highlight: true, order: :bottom) - full_message.should include "\e[1mTraceback\e[m (most recent call last)" - full_message.should include "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)" + full_message = e.full_message(highlight: true, order: :top).lines + full_message[0].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n" - full_message = e.full_message(highlight: false, order: :bottom) - full_message.should include "Traceback (most recent call last)" - full_message.should include "Some runtime error (RuntimeError)" + full_message = e.full_message(highlight: true, order: :bottom).lines + full_message[0].should == "\e[1mTraceback\e[m (most recent call last):\n" + full_message[-1].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n" + + full_message = e.full_message(highlight: false, order: :top).lines + full_message[0].should.end_with? "Some runtime error (RuntimeError)\n" + + full_message = e.full_message(highlight: false, order: :bottom).lines + full_message[0].should == "Traceback (most recent call last):\n" + full_message[-1].should.end_with? "Some runtime error (RuntimeError)\n" end it "supports :order option and places the error message and the backtrace at the top or the bottom" do @@ -35,9 +41,9 @@ describe "Exception#full_message" do it "shows the caller if the exception has no backtrace" do e = RuntimeError.new("Some runtime error") e.backtrace.should == nil - full_message = e.full_message(highlight: false, order: :top) - full_message.should include("#{__FILE__}:#{__LINE__-1}:in `") - full_message.should include("': Some runtime error (RuntimeError)\n") + full_message = e.full_message(highlight: false, order: :top).lines + full_message[0].should.start_with?("#{__FILE__}:#{__LINE__-1}:in `") + full_message[0].should.end_with?("': Some runtime error (RuntimeError)\n") end it "shows the exception class at the end of the first line of the message when the message contains multiple lines" do @@ -45,12 +51,24 @@ describe "Exception#full_message" do line = __LINE__; raise "first line\nsecond line" rescue => e full_message = e.full_message(highlight: false, order: :top).lines - full_message[0].should include("#{__FILE__}:#{line}:in `") - full_message[0].should include(": first line (RuntimeError)\n") + full_message[0].should.start_with?("#{__FILE__}:#{line}:in `") + full_message[0].should.end_with?(": first line (RuntimeError)\n") full_message[1].should == "second line\n" end end + it "highlights the entire message when the message contains multiple lines" do + begin + line = __LINE__; raise "first line\nsecond line\nthird line" + rescue => e + full_message = e.full_message(highlight: true, order: :top).lines + full_message[0].should.start_with?("#{__FILE__}:#{line}:in `") + full_message[0].should.end_with?(": \e[1mfirst line (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n") + full_message[1].should == "\e[1msecond line\e[m\n" + full_message[2].should == "\e[1mthird line\e[m\n" + end + end + it "contains cause of exception" do begin begin diff --git a/spec/ruby/core/exception/system_exit_spec.rb b/spec/ruby/core/exception/system_exit_spec.rb index eef9faf271..d899844c4e 100644 --- a/spec/ruby/core/exception/system_exit_spec.rb +++ b/spec/ruby/core/exception/system_exit_spec.rb @@ -1,6 +1,48 @@ require_relative '../../spec_helper' describe "SystemExit" do + describe "#initialize" do + it "accepts a status and message" do + exc = SystemExit.new(42, "message") + exc.status.should == 42 + exc.message.should == "message" + + exc = SystemExit.new(true, "message") + exc.status.should == 0 + exc.message.should == "message" + + exc = SystemExit.new(false, "message") + exc.status.should == 1 + exc.message.should == "message" + end + + it "accepts a status only" do + exc = SystemExit.new(42) + exc.status.should == 42 + exc.message.should == "SystemExit" + + exc = SystemExit.new(true) + exc.status.should == 0 + exc.message.should == "SystemExit" + + exc = SystemExit.new(false) + exc.status.should == 1 + exc.message.should == "SystemExit" + end + + it "accepts a message only" do + exc = SystemExit.new("message") + exc.status.should == 0 + exc.message.should == "message" + end + + it "accepts no arguments" do + exc = SystemExit.new + exc.status.should == 0 + exc.message.should == "SystemExit" + end + end + it "sets the exit status and exits silently when raised" do code = 'raise SystemExit.new(7)' result = ruby_exe(code, args: "2>&1", exit_status: 7) diff --git a/spec/ruby/core/file/utime_spec.rb b/spec/ruby/core/file/utime_spec.rb index 59eef20c66..a191e29240 100644 --- a/spec/ruby/core/file/utime_spec.rb +++ b/spec/ruby/core/file/utime_spec.rb @@ -77,6 +77,10 @@ describe "File.utime" do File.mtime(@file1).nsec.should.between?(0, 123500000) end + it "returns the number of filenames in the arguments" do + File.utime(@atime.to_f, @mtime.to_f, @file1, @file2).should == 2 + end + platform_is :linux do platform_is wordsize: 64 do it "allows Time instances in the far future to set mtime and atime (but some filesystems limit it up to 2446-05-10 or 2038-01-19)" do diff --git a/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb index 005886a482..13e8ffe4af 100644 --- a/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb +++ b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb @@ -40,6 +40,20 @@ ruby_version_is "2.7" do kw.should == h end + it "copies instance variables" do + h = {a: 1} + h.instance_variable_set(:@foo, 42) + kw = Hash.ruby2_keywords_hash(h) + kw.instance_variable_get(:@foo).should == 42 + end + + it "copies the hash internals" do + h = {a: 1} + kw = Hash.ruby2_keywords_hash(h) + h[:a] = 2 + kw[:a].should == 1 + end + it "raises TypeError for non-Hash" do -> { Hash.ruby2_keywords_hash(nil) }.should raise_error(TypeError) end diff --git a/spec/ruby/core/hash/shared/to_s.rb b/spec/ruby/core/hash/shared/to_s.rb index b0e3705d01..c74537aed4 100644 --- a/spec/ruby/core/hash/shared/to_s.rb +++ b/spec/ruby/core/hash/shared/to_s.rb @@ -2,7 +2,6 @@ require_relative '../../../spec_helper' require_relative '../fixtures/classes' describe :hash_to_s, shared: true do - it "returns a string representation with same order as each()" do h = { a: [1, 2], b: -2, d: -6, nil => nil } @@ -95,4 +94,8 @@ describe :hash_to_s, shared: true do {a: utf_16be}.send(@method).should == '{:a=>"utf_16be \u3042"}' end + + it "works for keys and values whose #inspect return a frozen String" do + { true => false }.to_s.should == "{true=>false}" + end end diff --git a/spec/ruby/core/io/shared/readlines.rb b/spec/ruby/core/io/shared/readlines.rb index 8e4a73bb98..52b20364ef 100644 --- a/spec/ruby/core/io/shared/readlines.rb +++ b/spec/ruby/core/io/shared/readlines.rb @@ -73,6 +73,11 @@ describe :io_readlines_options_19, shared: true do result = IO.send(@method, @name, 10, &@object) (result ? result : ScratchPad.recorded).should == IOSpecs.lines_limit end + + it "ignores the object as a limit if it is negative" do + result = IO.send(@method, @name, -2, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end end describe "when the object is a String" do diff --git a/spec/ruby/core/kernel/define_singleton_method_spec.rb b/spec/ruby/core/kernel/define_singleton_method_spec.rb index dc77c3e6f8..2d8b1bf413 100644 --- a/spec/ruby/core/kernel/define_singleton_method_spec.rb +++ b/spec/ruby/core/kernel/define_singleton_method_spec.rb @@ -96,4 +96,19 @@ describe "Kernel#define_singleton_method" do o.define(:foo) { raise "not used" } }.should raise_error(ArgumentError) end + + it "always defines the method with public visibility" do + cls = Class.new + def cls.define(name, &block) + private + define_singleton_method(name, &block) + end + + -> { + suppress_warning do + cls.define(:foo) { :ok } + end + cls.foo.should == :ok + }.should_not raise_error(NoMethodError) + end end diff --git a/spec/ruby/core/math/sqrt_spec.rb b/spec/ruby/core/math/sqrt_spec.rb index 779aea2a0a..918e7c3a17 100644 --- a/spec/ruby/core/math/sqrt_spec.rb +++ b/spec/ruby/core/math/sqrt_spec.rb @@ -27,6 +27,10 @@ describe "Math.sqrt" do it "accepts any argument that can be coerced with Float()" do Math.sqrt(MathSpecs::Float.new).should be_close(1.0, TOLERANCE) end + + it "raises a Math::DomainError when given a negative number" do + -> { Math.sqrt(-1) }.should raise_error(Math::DomainError) + end end describe "Math#sqrt" do diff --git a/spec/ruby/core/module/const_get_spec.rb b/spec/ruby/core/module/const_get_spec.rb index 9f9fafe5bb..69f181cf51 100644 --- a/spec/ruby/core/module/const_get_spec.rb +++ b/spec/ruby/core/module/const_get_spec.rb @@ -100,6 +100,16 @@ describe "Module#const_get" do ConstantSpecs.const_get("::CS_CONST1").should == :const1 end + it "accepts a toplevel scope qualifier when inherit is false" do + ConstantSpecs.const_get("::CS_CONST1", false).should == :const1 + -> { ConstantSpecs.const_get("CS_CONST1", false) }.should raise_error(NameError) + end + + it "returns a constant whose module is defined the the toplevel" do + ConstantSpecs.const_get("ConstantSpecsTwo::Foo").should == :cs_two_foo + ConstantSpecsThree.const_get("ConstantSpecsTwo::Foo").should == :cs_three_foo + end + it "accepts a scoped constant name" do ConstantSpecs.const_get("ClassA::CS_CONST10").should == :const10_10 end @@ -140,6 +150,10 @@ describe "Module#const_get" do Object.const_get('CSAutoloadD::InnerModule').name.should == 'CSAutoloadD::InnerModule' end + it "raises a NameError when the nested constant does not exist on the module but exists in Object" do + -> { Object.const_get('ConstantSpecs::CS_CONST1') }.should raise_error(NameError) + end + describe "with statically assigned constants" do it "searches the immediate class or module first" do ConstantSpecs::ClassA.const_get(:CS_CONST10).should == :const10_10 diff --git a/spec/ruby/core/module/ruby2_keywords_spec.rb b/spec/ruby/core/module/ruby2_keywords_spec.rb index 996774891c..6de3fdec80 100644 --- a/spec/ruby/core/module/ruby2_keywords_spec.rb +++ b/spec/ruby/core/module/ruby2_keywords_spec.rb @@ -15,6 +15,83 @@ ruby_version_is "2.7" do Hash.ruby2_keywords_hash?(last).should == true end + it "makes a copy of the hash and only marks the copy as keyword hash" do + obj = Object.new + obj.singleton_class.class_exec do + def regular(*args) + args.last + end + + ruby2_keywords def foo(*args) + args.last + end + end + + h = {a: 1} + ruby_version_is "3.0" do + obj.regular(**h).should.equal?(h) + end + + last = obj.foo(**h) + Hash.ruby2_keywords_hash?(last).should == true + Hash.ruby2_keywords_hash?(h).should == false + + last2 = obj.foo(**last) # last is already marked + Hash.ruby2_keywords_hash?(last2).should == true + Hash.ruby2_keywords_hash?(last).should == true + last2.should_not.equal?(last) + Hash.ruby2_keywords_hash?(h).should == false + end + + it "makes a copy and unmark at the call site when calling with marked *args" do + obj = Object.new + obj.singleton_class.class_exec do + ruby2_keywords def foo(*args) + args + end + + def single(arg) + arg + end + + def splat(*args) + args.last + end + + def kwargs(**kw) + kw + end + end + + h = { a: 1 } + args = obj.foo(**h) + marked = args.last + Hash.ruby2_keywords_hash?(marked).should == true + + after_usage = obj.single(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + + after_usage = obj.splat(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + ruby_bug "#18625", ""..."3.3" do # might be fixed in 3.2 + Hash.ruby2_keywords_hash?(after_usage).should == false + end + Hash.ruby2_keywords_hash?(marked).should == true + + after_usage = obj.kwargs(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + end + it "applies to the underlying method and applies across aliasing" do obj = Object.new @@ -80,7 +157,7 @@ ruby_version_is "2.7" do }.should raise_error(NameError, /undefined method `not_existing'/) end - it "acceps String as well" do + it "accepts String as well" do obj = Object.new obj.singleton_class.class_exec do diff --git a/spec/ruby/core/proc/parameters_spec.rb b/spec/ruby/core/proc/parameters_spec.rb index 3ced7b22ab..3a56b613cd 100644 --- a/spec/ruby/core/proc/parameters_spec.rb +++ b/spec/ruby/core/proc/parameters_spec.rb @@ -31,7 +31,7 @@ describe "Proc#parameters" do end it "regards named parameters in lambda as optional if lambda: false keyword used" do - lambda {|x| }.parameters(lambda: false).first.first.should == :opt + -> x { }.parameters(lambda: false).first.first.should == :opt end end diff --git a/spec/ruby/core/random/rand_spec.rb b/spec/ruby/core/random/rand_spec.rb index 6ea7eece5f..9244177ab5 100644 --- a/spec/ruby/core/random/rand_spec.rb +++ b/spec/ruby/core/random/rand_spec.rb @@ -205,6 +205,11 @@ describe "Random#rand with Range" do Random.new(42).rand(0..1.0).should be_kind_of(Float) end + it "returns a float within a given float range" do + Random.new(42).rand(0.0...100.0).should == 37.454011884736246 + Random.new(42).rand(-100.0...0.0).should == -62.545988115263754 + end + it "raises an ArgumentError when the startpoint lacks #+ and #- methods" do -> do Random.new.rand(Object.new..67) diff --git a/spec/ruby/core/string/append_spec.rb b/spec/ruby/core/string/append_spec.rb index 1e1667f617..e001257621 100644 --- a/spec/ruby/core/string/append_spec.rb +++ b/spec/ruby/core/string/append_spec.rb @@ -5,4 +5,9 @@ require_relative 'shared/concat' describe "String#<<" do it_behaves_like :string_concat, :<< it_behaves_like :string_concat_encoding, :<< + + it "raises an ArgumentError when given the incorrect number of arguments" do + -> { "hello".send(:<<) }.should raise_error(ArgumentError) + -> { "hello".send(:<<, "one", "two") }.should raise_error(ArgumentError) + end end diff --git a/spec/ruby/core/string/comparison_spec.rb b/spec/ruby/core/string/comparison_spec.rb index 01199274b6..91cfdca25a 100644 --- a/spec/ruby/core/string/comparison_spec.rb +++ b/spec/ruby/core/string/comparison_spec.rb @@ -75,6 +75,10 @@ describe "String#<=> with String" do (xff_1 <=> xff_2).should == -1 (xff_2 <=> xff_1).should == 1 end + + it "returns 0 when comparing 2 empty strings but one is not ASCII-compatible" do + ("" <=> "".force_encoding('iso-2022-jp')).should == 0 + end end # Note: This is inconsistent with Array#<=> which calls #to_ary instead of diff --git a/spec/ruby/core/string/lstrip_spec.rb b/spec/ruby/core/string/lstrip_spec.rb index 20e4cdeabd..8b5dd1b467 100644 --- a/spec/ruby/core/string/lstrip_spec.rb +++ b/spec/ruby/core/string/lstrip_spec.rb @@ -12,7 +12,7 @@ describe "String#lstrip" do "hello".lstrip.should == "hello" end - ruby_version_is '3.1' do + ruby_version_is '3.0' do it "strips leading \\0" do "\x00hello".lstrip.should == "hello" "\000 \000hello\000 \000".lstrip.should == "hello\000 \000" @@ -35,7 +35,7 @@ describe "String#lstrip!" do a.should == "hello " end - ruby_version_is '3.1' do + ruby_version_is '3.0' do it "strips leading \\0" do a = "\000 \000hello\000 \000" a.lstrip! diff --git a/spec/ruby/core/string/rindex_spec.rb b/spec/ruby/core/string/rindex_spec.rb index 7a6af0c9d0..a3b437a1e4 100644 --- a/spec/ruby/core/string/rindex_spec.rb +++ b/spec/ruby/core/string/rindex_spec.rb @@ -4,13 +4,17 @@ require_relative 'fixtures/classes' require_relative 'fixtures/utf-8-encoding' describe "String#rindex with object" do - it "raises a TypeError if obj isn't a String, Integer or Regexp" do + it "raises a TypeError if obj isn't a String or Regexp" do not_supported_on :opal do -> { "hello".rindex(:sym) }.should raise_error(TypeError) end -> { "hello".rindex(mock('x')) }.should raise_error(TypeError) end + it "raises a TypeError if obj is an Integer" do + -> { "hello".rindex(42) }.should raise_error(TypeError) + end + it "doesn't try to convert obj to an integer via to_int" do obj = mock('x') obj.should_not_receive(:to_int) diff --git a/spec/ruby/core/string/shared/eql.rb b/spec/ruby/core/string/shared/eql.rb index b57d6895ff..6f268c929c 100644 --- a/spec/ruby/core/string/shared/eql.rb +++ b/spec/ruby/core/string/shared/eql.rb @@ -31,4 +31,8 @@ describe :string_eql_value, shared: true do a.send(@method, b).should be_true b.send(@method, a).should be_true end + + it "returns true when comparing 2 empty strings but one is not ASCII-compatible" do + "".send(@method, "".force_encoding('iso-2022-jp')).should == true + end end diff --git a/spec/ruby/core/string/split_spec.rb b/spec/ruby/core/string/split_spec.rb index 2e03955a26..2da71948b3 100644 --- a/spec/ruby/core/string/split_spec.rb +++ b/spec/ruby/core/string/split_spec.rb @@ -589,4 +589,11 @@ describe "String#split with Regexp" do end end end + + it "raises a TypeError when not called with nil, String, or Regexp" do + -> { "hello".split(42) }.should raise_error(TypeError) + -> { "hello".split(:ll) }.should raise_error(TypeError) + -> { "hello".split(false) }.should raise_error(TypeError) + -> { "hello".split(Object.new) }.should raise_error(TypeError) + end end diff --git a/spec/ruby/core/string/strip_spec.rb b/spec/ruby/core/string/strip_spec.rb index 8517bf2d2a..da06862d06 100644 --- a/spec/ruby/core/string/strip_spec.rb +++ b/spec/ruby/core/string/strip_spec.rb @@ -11,7 +11,7 @@ describe "String#strip" do "\tgoodbye\r\v\n".strip.should == "goodbye" end - ruby_version_is '3.1' do + ruby_version_is '3.0' do it "returns a copy of self without leading and trailing NULL bytes and whitespace" do " \x00 goodbye \x00 ".strip.should == "goodbye" end @@ -43,7 +43,7 @@ describe "String#strip!" do a.should == "hello" end - ruby_version_is '3.1' do + ruby_version_is '3.0' do it "removes leading and trailing NULL bytes and whitespace" do a = "\000 goodbye \000" a.strip! diff --git a/spec/ruby/core/struct/new_spec.rb b/spec/ruby/core/struct/new_spec.rb index fdbf8c2c91..7b4a4f7980 100644 --- a/spec/ruby/core/struct/new_spec.rb +++ b/spec/ruby/core/struct/new_spec.rb @@ -60,14 +60,21 @@ describe "Struct.new" do -> { Struct.new(:animal, nil) }.should raise_error(TypeError) -> { Struct.new(:animal, true) }.should raise_error(TypeError) -> { Struct.new(:animal, ['chris', 'evan']) }.should raise_error(TypeError) - ruby_version_is "3.2" do - -> { Struct.new(:animal, { name: 'chris' }) }.should raise_error(TypeError) - end end ruby_version_is ""..."3.2" do - it "raises a ArgumentError if passed a Hash with an unknown key" do - -> { Struct.new(:animal, { name: 'chris' }) }.should raise_error(ArgumentError) + it "raises a TypeError or ArgumentError if passed a Hash with an unknown key" do + # CRuby < 3.2 raises ArgumentError: unknown keyword: :name, but that seems a bug: + # https://bugs.ruby-lang.org/issues/18632 + -> { Struct.new(:animal, { name: 'chris' }) }.should raise_error(StandardError) { |e| + [ArgumentError, TypeError].should.include?(e.class) + } + end + end + + ruby_version_is "3.2" do + it "raises a TypeError if passed a Hash with an unknown key" do + -> { Struct.new(:animal, { name: 'chris' }) }.should raise_error(TypeError) end end diff --git a/spec/ruby/core/thread/shared/exit.rb b/spec/ruby/core/thread/shared/exit.rb index 40dc478947..3663827579 100644 --- a/spec/ruby/core/thread/shared/exit.rb +++ b/spec/ruby/core/thread/shared/exit.rb @@ -66,6 +66,26 @@ describe :thread_exit, shared: true do ScratchPad.recorded.should == nil end + it "does not reset $!" do + ScratchPad.record [] + + exc = RuntimeError.new("foo") + thread = Thread.new do + begin + raise exc + ensure + ScratchPad << $! + begin + Thread.current.send(@method) + ensure + ScratchPad << $! + end + end + end + thread.join + ScratchPad.recorded.should == [exc, exc] + end + it "cannot be rescued" do thread = Thread.new do begin @@ -73,7 +93,7 @@ describe :thread_exit, shared: true do rescue Exception ScratchPad.record :in_rescue end - ScratchPad.record :end_of_thread_block + ScratchPad.record :end_of_thread_block end thread.join diff --git a/spec/ruby/fixtures/constants.rb b/spec/ruby/fixtures/constants.rb index 37271ddcc8..4c456dae89 100644 --- a/spec/ruby/fixtures/constants.rb +++ b/spec/ruby/fixtures/constants.rb @@ -299,4 +299,14 @@ module ConstantSpecs private_constant :CS_PRIVATE end +module ConstantSpecsThree + module ConstantSpecsTwo + Foo = :cs_three_foo + end +end + +module ConstantSpecsTwo + Foo = :cs_two_foo +end + include ConstantSpecs::ModuleA diff --git a/spec/ruby/language/keyword_arguments_spec.rb b/spec/ruby/language/keyword_arguments_spec.rb new file mode 100644 index 0000000000..66e37915a2 --- /dev/null +++ b/spec/ruby/language/keyword_arguments_spec.rb @@ -0,0 +1,307 @@ +require_relative '../spec_helper' + +ruby_version_is "3.0" do + describe "Keyword arguments" do + def target(*args, **kwargs) + [args, kwargs] + end + + it "are separated from positional arguments" do + def m(*args, **kwargs) + [args, kwargs] + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "when the receiving method has not keyword parameters it treats kwargs as positional" do + def m(*a) + a + end + + m(a: 1).should == [{a: 1}] + m({a: 1}).should == [{a: 1}] + end + + context "empty kwargs are treated as if they were not passed" do + it "when calling a method" do + def m(*a) + a + end + + empty = {} + m(**empty).should == [] + m(empty).should == [{}] + end + + it "when yielding to a block" do + def y(*args, **kwargs) + yield(*args, **kwargs) + end + + empty = {} + y(**empty) { |*a| a }.should == [] + y(empty) { |*a| a }.should == [{}] + end + end + + it "extra keywords are not allowed without **kwrest" do + def m(*a, kw:) + a + end + + 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') + end + + it "handle * and ** at the same call site" do + def m(*a) + a + end + + m(*[], **{}).should == [] + m(*[], 42, **{}).should == [42] + end + + context "**" do + it "does not copy a non-empty Hash for a method taking (*args)" do + def m(*args) + args[0] + end + + h = {a: 1} + m(**h).should.equal?(h) + end + + it "copies the given Hash for a method taking (**kwargs)" do + def m(**kw) + kw + end + + empty = {} + m(**empty).should == empty + m(**empty).should_not.equal?(empty) + + h = {a: 1} + m(**h).should == h + m(**h).should_not.equal?(h) + end + end + + context "delegation" do + it "works with (*args, **kwargs)" do + def m(*args, **kwargs) + target(*args, **kwargs) + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "works with proc { |*args, **kwargs| }" do + m = proc do |*args, **kwargs| + target(*args, **kwargs) + end + + empty = {} + m.(**empty).should == [[], {}] + m.(empty).should == [[{}], {}] + + m.(a: 1).should == [[], {a: 1}] + m.({a: 1}).should == [[{a: 1}], {}] + + # no autosplatting for |*args, **kwargs| + m.([1, 2]).should == [[[1, 2]], {}] + end + + it "works with -> (*args, **kwargs) {}" do + m = -> *args, **kwargs do + target(*args, **kwargs) + end + + empty = {} + m.(**empty).should == [[], {}] + m.(empty).should == [[{}], {}] + + m.(a: 1).should == [[], {a: 1}] + m.({a: 1}).should == [[{a: 1}], {}] + end + + it "works with (...)" do + instance_eval <<~DEF + def m(...) + target(...) + end + DEF + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "works with call(*ruby2_keyword_args)" do + class << self + ruby2_keywords def m(*args) + target(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + m(**kw).should == [[], {a: 1}] + m(**kw)[1].should == kw + m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(m(**kw)[1]).should == false + + m(kw).should == [[{a: 1}], {}] + m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with super(*ruby2_keyword_args)" do + parent = Class.new do + def m(*args, **kwargs) + [args, kwargs] + end + end + + child = Class.new(parent) do + ruby2_keywords def m(*args) + super(*args) + end + end + + obj = child.new + + empty = {} + obj.m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + obj.m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + obj.m(a: 1).should == [[], {a: 1}] + obj.m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + obj.m(**kw).should == [[], {a: 1}] + obj.m(**kw)[1].should == kw + obj.m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(obj.m(**kw)[1]).should == false + + obj.m(kw).should == [[{a: 1}], {}] + obj.m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with zsuper" do + parent = Class.new do + def m(*args, **kwargs) + [args, kwargs] + end + end + + child = Class.new(parent) do + ruby2_keywords def m(*args) + super + end + end + + obj = child.new + + empty = {} + obj.m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + obj.m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + obj.m(a: 1).should == [[], {a: 1}] + obj.m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + obj.m(**kw).should == [[], {a: 1}] + obj.m(**kw)[1].should == kw + obj.m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(obj.m(**kw)[1]).should == false + + obj.m(kw).should == [[{a: 1}], {}] + obj.m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with yield(*ruby2_keyword_args)" do + class << self + def y(args) + yield(*args) + end + + ruby2_keywords def m(*outer_args) + y(outer_args, &-> *args, **kwargs { target(*args, **kwargs) }) + end + end + + empty = {} + m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + m(**kw).should == [[], {a: 1}] + m(**kw)[1].should == kw + m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(m(**kw)[1]).should == false + + m(kw).should == [[{a: 1}], {}] + m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "does not work with (*args)" do + class << self + def m(*args) + target(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[{a: 1}], {}] + m({a: 1}).should == [[{a: 1}], {}] + end + end + end +end diff --git a/spec/ruby/library/logger/device/close_spec.rb b/spec/ruby/library/logger/device/close_spec.rb index d7d107fcce..7c5e118d56 100644 --- a/spec/ruby/library/logger/device/close_spec.rb +++ b/spec/ruby/library/logger/device/close_spec.rb @@ -15,14 +15,14 @@ describe "Logger::LogDevice#close" do rm_r @file_path end - ruby_version_is ""..."2.7" do + version_is Logger::VERSION, ""..."1.4.0" do it "closes the LogDevice's stream" do @device.close -> { @device.write("Test") }.should complain(/\Alog writing failed\./) end end - ruby_version_is "2.7" do + version_is Logger::VERSION, "1.4.0" do it "closes the LogDevice's stream" do @device.close -> { @device.write("Test") }.should complain(/\Alog shifting failed\./) diff --git a/spec/ruby/library/logger/device/write_spec.rb b/spec/ruby/library/logger/device/write_spec.rb index 5506bb2c38..cd2d7e27a9 100644 --- a/spec/ruby/library/logger/device/write_spec.rb +++ b/spec/ruby/library/logger/device/write_spec.rb @@ -35,14 +35,14 @@ describe "Logger::LogDevice#write" do rm_r path end - ruby_version_is ""..."2.7" do + version_is Logger::VERSION, ""..."1.4.0" do it "fails if the device is already closed" do @device.close -> { @device.write "foo" }.should complain(/\Alog writing failed\./) end end - ruby_version_is "2.7" do + version_is Logger::VERSION, "1.4.0" do it "fails if the device is already closed" do @device.close -> { @device.write "foo" }.should complain(/\Alog shifting failed\./) diff --git a/spec/ruby/optional/capi/class_spec.rb b/spec/ruby/optional/capi/class_spec.rb index a2d8b3e38a..a57b8f644f 100644 --- a/spec/ruby/optional/capi/class_spec.rb +++ b/spec/ruby/optional/capi/class_spec.rb @@ -108,17 +108,37 @@ describe "C-API Class function" do describe "rb_class_new_instance" do it "allocates and initializes a new object" do - o = @s.rb_class_new_instance(0, nil, CApiClassSpecs::Alloc) + o = @s.rb_class_new_instance([], CApiClassSpecs::Alloc) o.class.should == CApiClassSpecs::Alloc o.initialized.should be_true end it "passes arguments to the #initialize method" do - o = @s.rb_class_new_instance(2, [:one, :two], CApiClassSpecs::Alloc) + o = @s.rb_class_new_instance([:one, :two], CApiClassSpecs::Alloc) o.arguments.should == [:one, :two] end end + ruby_version_is "3.0" do + describe "rb_class_new_instance_kw" do + it "passes arguments and keywords to the #initialize method" do + obj = @s.rb_class_new_instance_kw([{pos: 1}, {kw: 2}], CApiClassSpecs::KeywordAlloc) + obj.args.should == [{pos: 1}] + obj.kwargs.should == {kw: 2} + + obj = @s.rb_class_new_instance_kw([{}], CApiClassSpecs::KeywordAlloc) + obj.args.should == [] + obj.kwargs.should == {} + end + + it "raises TypeError if the last argument is not a Hash" do + -> { + @s.rb_class_new_instance_kw([42], CApiClassSpecs::KeywordAlloc) + }.should raise_error(TypeError, 'no implicit conversion of Integer into Hash') + end + end + end + describe "rb_include_module" do it "includes a module into a class" do c = Class.new diff --git a/spec/ruby/optional/capi/ext/class_spec.c b/spec/ruby/optional/capi/ext/class_spec.c index 36b8c8f2f3..b860742906 100644 --- a/spec/ruby/optional/capi/ext/class_spec.c +++ b/spec/ruby/optional/capi/ext/class_spec.c @@ -61,19 +61,16 @@ static VALUE class_spec_rb_class_new(VALUE self, VALUE super) { return rb_class_new(super); } -static VALUE class_spec_rb_class_new_instance(VALUE self, - VALUE nargs, VALUE args, - VALUE klass) { - int c_nargs = FIX2INT(nargs); - VALUE *c_args = (VALUE*)alloca(sizeof(VALUE) * c_nargs); - int i; - - for (i = 0; i < c_nargs; i++) - c_args[i] = rb_ary_entry(args, i); - - return rb_class_new_instance(c_nargs, c_args, klass); +static VALUE class_spec_rb_class_new_instance(VALUE self, VALUE args, VALUE klass) { + return rb_class_new_instance(RARRAY_LENINT(args), RARRAY_PTR(args), klass); } +#ifdef RUBY_VERSION_IS_3_0 +static VALUE class_spec_rb_class_new_instance_kw(VALUE self, VALUE args, VALUE klass) { + return rb_class_new_instance_kw(RARRAY_LENINT(args), RARRAY_PTR(args), klass, RB_PASS_KEYWORDS); +} +#endif + static VALUE class_spec_rb_class_real(VALUE self, VALUE object) { if(rb_type_p(object, T_FIXNUM)) { return INT2FIX(rb_class_real(FIX2INT(object))); @@ -171,7 +168,10 @@ void Init_class_spec(void) { rb_define_method(cls, "rb_class_protected_instance_methods", class_spec_rb_class_protected_instance_methods, -1); rb_define_method(cls, "rb_class_private_instance_methods", class_spec_rb_class_private_instance_methods, -1); rb_define_method(cls, "rb_class_new", class_spec_rb_class_new, 1); - rb_define_method(cls, "rb_class_new_instance", class_spec_rb_class_new_instance, 3); + rb_define_method(cls, "rb_class_new_instance", class_spec_rb_class_new_instance, 2); +#ifdef RUBY_VERSION_IS_3_0 + rb_define_method(cls, "rb_class_new_instance_kw", class_spec_rb_class_new_instance_kw, 2); +#endif rb_define_method(cls, "rb_class_real", class_spec_rb_class_real, 1); rb_define_method(cls, "rb_class_superclass", class_spec_rb_class_superclass, 1); rb_define_method(cls, "rb_cvar_defined", class_spec_cvar_defined, 2); diff --git a/spec/ruby/optional/capi/ext/kernel_spec.c b/spec/ruby/optional/capi/ext/kernel_spec.c index bbfeb198b7..46af8696a5 100644 --- a/spec/ruby/optional/capi/ext/kernel_spec.c +++ b/spec/ruby/optional/capi/ext/kernel_spec.c @@ -142,7 +142,7 @@ VALUE kernel_spec_call_proc_with_raised_exc(VALUE arg_array, VALUE raised_exc) { argc = 2; - return rb_funcall2(proc, rb_intern("call"), argc, argv); + return rb_funcallv(proc, rb_intern("call"), argc, argv); } VALUE kernel_spec_rb_rescue(VALUE self, VALUE main_proc, VALUE arg, @@ -318,8 +318,22 @@ static VALUE kernel_spec_rb_make_backtrace(VALUE self) { return rb_make_backtrace(); } -static VALUE kernel_spec_rb_funcall3(VALUE self, VALUE obj, VALUE method) { - return rb_funcall3(obj, SYM2ID(method), 0, NULL); +static VALUE kernel_spec_rb_funcallv(VALUE self, VALUE obj, VALUE method, VALUE args) { + return rb_funcallv(obj, SYM2ID(method), RARRAY_LENINT(args), RARRAY_PTR(args)); +} + +#ifdef RUBY_VERSION_IS_3_0 +static VALUE kernel_spec_rb_funcallv_kw(VALUE self, VALUE obj, VALUE method, VALUE args) { + return rb_funcallv_kw(obj, SYM2ID(method), RARRAY_LENINT(args), RARRAY_PTR(args), RB_PASS_KEYWORDS); +} + +static VALUE kernel_spec_rb_keyword_given_p(int argc, VALUE *args, VALUE self) { + return rb_keyword_given_p() ? Qtrue : Qfalse; +} +#endif + +static VALUE kernel_spec_rb_funcallv_public(VALUE self, VALUE obj, VALUE method) { + return rb_funcallv_public(obj, SYM2ID(method), 0, NULL); } static VALUE kernel_spec_rb_funcall_with_block(VALUE self, VALUE obj, VALUE method, VALUE block) { @@ -371,7 +385,12 @@ void Init_kernel_spec(void) { rb_define_method(cls, "rb_set_end_proc", kernel_spec_rb_set_end_proc, 1); rb_define_method(cls, "rb_f_sprintf", kernel_spec_rb_f_sprintf, 1); rb_define_method(cls, "rb_make_backtrace", kernel_spec_rb_make_backtrace, 0); - rb_define_method(cls, "rb_funcall3", kernel_spec_rb_funcall3, 2); + rb_define_method(cls, "rb_funcallv", kernel_spec_rb_funcallv, 3); +#ifdef RUBY_VERSION_IS_3_0 + rb_define_method(cls, "rb_funcallv_kw", kernel_spec_rb_funcallv_kw, 3); + rb_define_method(cls, "rb_keyword_given_p", kernel_spec_rb_keyword_given_p, -1); +#endif + rb_define_method(cls, "rb_funcallv_public", kernel_spec_rb_funcallv_public, 2); rb_define_method(cls, "rb_funcall_many_args", kernel_spec_rb_funcall_many_args, 2); rb_define_method(cls, "rb_funcall_with_block", kernel_spec_rb_funcall_with_block, 3); } diff --git a/spec/ruby/optional/capi/ext/symbol_spec.c b/spec/ruby/optional/capi/ext/symbol_spec.c index 7d9a7b4379..ba88635faa 100644 --- a/spec/ruby/optional/capi/ext/symbol_spec.c +++ b/spec/ruby/optional/capi/ext/symbol_spec.c @@ -47,10 +47,19 @@ VALUE symbol_spec_rb_id2name(VALUE self, VALUE symbol) { return rb_str_new(c_str, strlen(c_str)); } +VALUE symbol_spec_rb_id2name_id_zero(VALUE self) { + const char* c_str = rb_id2name((ID) 0); + return c_str ? rb_str_new(c_str, strlen(c_str)) : Qnil; +} + VALUE symbol_spec_rb_id2str(VALUE self, VALUE symbol) { return rb_id2str(SYM2ID(symbol)); } +VALUE symbol_spec_rb_id2str_id_zero(VALUE self) { + return rb_id2str((ID) 0); +} + VALUE symbol_spec_rb_intern_str(VALUE self, VALUE str) { return ID2SYM(rb_intern_str(str)); } @@ -90,7 +99,9 @@ void Init_symbol_spec(void) { rb_define_method(cls, "rb_intern3", symbol_spec_rb_intern3, 3); rb_define_method(cls, "rb_intern3_c_compare", symbol_spec_rb_intern3_c_compare, 4); rb_define_method(cls, "rb_id2name", symbol_spec_rb_id2name, 1); + rb_define_method(cls, "rb_id2name_id_zero", symbol_spec_rb_id2name_id_zero, 0); rb_define_method(cls, "rb_id2str", symbol_spec_rb_id2str, 1); + rb_define_method(cls, "rb_id2str_id_zero", symbol_spec_rb_id2str_id_zero, 0); rb_define_method(cls, "rb_intern_str", symbol_spec_rb_intern_str, 1); rb_define_method(cls, "rb_check_symbol_cstr", symbol_spec_rb_check_symbol_cstr, 1); rb_define_method(cls, "rb_is_class_id", symbol_spec_rb_is_class_id, 1); diff --git a/spec/ruby/optional/capi/fixtures/class.rb b/spec/ruby/optional/capi/fixtures/class.rb index 193c7174e0..b463e3b4c3 100644 --- a/spec/ruby/optional/capi/fixtures/class.rb +++ b/spec/ruby/optional/capi/fixtures/class.rb @@ -15,6 +15,16 @@ class CApiClassSpecs end end + class KeywordAlloc + attr_reader :initialized, :args, :kwargs + + def initialize(*args, **kwargs) + @initialized = true + @args = args + @kwargs = kwargs + end + end + class Attr def initialize @foo, @bar, @baz = 1, 2, 3 diff --git a/spec/ruby/optional/capi/kernel_spec.rb b/spec/ruby/optional/capi/kernel_spec.rb index 758d944da9..88545265d0 100644 --- a/spec/ruby/optional/capi/kernel_spec.rb +++ b/spec/ruby/optional/capi/kernel_spec.rb @@ -567,7 +567,64 @@ describe "C-API Kernel function" do end end - describe "rb_funcall3" do + describe "rb_funcallv" do + def empty + 42 + end + + def sum(a, b) + a + b + end + + it "calls a method" do + @s.rb_funcallv(self, :empty, []).should == 42 + @s.rb_funcallv(self, :sum, [1, 2]).should == 3 + end + end + + ruby_version_is "3.0" do + describe "rb_funcallv_kw" do + it "passes keyword arguments to the callee" do + def m(*args, **kwargs) + [args, kwargs] + end + + @s.rb_funcallv_kw(self, :m, [{}]).should == [[], {}] + @s.rb_funcallv_kw(self, :m, [{a: 1}]).should == [[], {a: 1}] + @s.rb_funcallv_kw(self, :m, [{b: 2}, {a: 1}]).should == [[{b: 2}], {a: 1}] + @s.rb_funcallv_kw(self, :m, [{b: 2}, {}]).should == [[{b: 2}], {}] + end + + it "raises TypeError if the last argument is not a Hash" do + def m(*args, **kwargs) + [args, kwargs] + end + + -> { + @s.rb_funcallv_kw(self, :m, [42]) + }.should raise_error(TypeError, 'no implicit conversion of Integer into Hash') + end + end + + describe "rb_keyword_given_p" do + it "returns whether keywords were given to the C extension method" do + h = {a: 1} + empty = {} + @s.rb_keyword_given_p(a: 1).should == true + @s.rb_keyword_given_p("foo" => "bar").should == true + @s.rb_keyword_given_p(**h).should == true + + @s.rb_keyword_given_p(h).should == false + @s.rb_keyword_given_p().should == false + @s.rb_keyword_given_p(**empty).should == false + + @s.rb_funcallv_kw(@s, :rb_keyword_given_p, [{a: 1}]).should == true + @s.rb_funcallv_kw(@s, :rb_keyword_given_p, [{}]).should == false + end + end + end + + describe "rb_funcallv_public" do before :each do @obj = Object.new class << @obj @@ -578,10 +635,11 @@ describe "C-API Kernel function" do end it "calls a public method" do - @s.rb_funcall3(@obj, :method_public).should == :method_public + @s.rb_funcallv_public(@obj, :method_public).should == :method_public end + it "does not call a private method" do - -> { @s.rb_funcall3(@obj, :method_private) }.should raise_error(NoMethodError, /private/) + -> { @s.rb_funcallv_public(@obj, :method_private) }.should raise_error(NoMethodError, /private/) end end @@ -599,6 +657,7 @@ describe "C-API Kernel function" do @s.rb_funcall_many_args(@obj, :many_args).should == 15.downto(1).to_a end end + describe 'rb_funcall_with_block' do before :each do @obj = Object.new diff --git a/spec/ruby/optional/capi/symbol_spec.rb b/spec/ruby/optional/capi/symbol_spec.rb index b8fda34c0e..12c93c9f27 100644 --- a/spec/ruby/optional/capi/symbol_spec.rb +++ b/spec/ruby/optional/capi/symbol_spec.rb @@ -61,6 +61,10 @@ describe "C-API Symbol function" do it "converts a symbol to a C char array" do @s.rb_id2name(:test_symbol).should == "test_symbol" end + + it "returns (char*) NULL for (ID) 0" do + @s.rb_id2name_id_zero.should == nil + end end describe "rb_id2str" do @@ -72,6 +76,10 @@ describe "C-API Symbol function" do str = "test_symbol".encode(Encoding::UTF_16LE) @s.rb_id2str(str.to_sym).encoding.should == Encoding::UTF_16LE end + + it "returns (VALUE) 0 = Qfalse for (ID) 0" do + @s.rb_id2str_id_zero.should == false + end end describe "rb_intern_str" do diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb index 765ba0f929..82fb0333c8 100644 --- a/spec/ruby/shared/kernel/raise.rb +++ b/spec/ruby/shared/kernel/raise.rb @@ -29,11 +29,41 @@ describe :kernel_raise, shared: true do @data = data end end - -> { @object.raise(data_error, {:data => 42}) }.should raise_error(data_error) do |ex| - ex.data.should == {:data => 42} + + -> { @object.raise(data_error, {data: 42}) }.should raise_error(data_error) do |ex| + ex.data.should == {data: 42} end end + # https://bugs.ruby-lang.org/issues/8257#note-36 + it "allows extra keyword arguments for compatibility" do + data_error = Class.new(StandardError) do + attr_reader :data + def initialize(data) + @data = data + end + end + + -> { @object.raise(data_error, data: 42) }.should raise_error(data_error) do |ex| + ex.data.should == {data: 42} + end + end + + it "does not allow message and extra keyword arguments" do + data_error = Class.new(StandardError) do + attr_reader :data + def initialize(data) + @data = data + end + end + + -> { @object.raise(data_error, {a: 1}, b: 2) }.should raise_error(StandardError) do |e| + [TypeError, ArgumentError].should.include?(e.class) + end + + -> { @object.raise(data_error, {a: 1}, [], b: 2) }.should raise_error(ArgumentError) + end + it "raises RuntimeError if no exception class is given" do -> { @object.raise }.should raise_error(RuntimeError, "") end diff --git a/spec/ruby/shared/process/exit.rb b/spec/ruby/shared/process/exit.rb index ae8abaea40..7d901f1f1e 100644 --- a/spec/ruby/shared/process/exit.rb +++ b/spec/ruby/shared/process/exit.rb @@ -21,6 +21,12 @@ describe :process_exit, shared: true do end end + it "raises a SystemExit with message 'exit'" do + -> { @object.exit }.should raise_error(SystemExit) { |e| + e.message.should == "exit" + } + end + it "tries to convert the passed argument to an Integer using #to_int" do obj = mock('5') obj.should_receive(:to_int).and_return(5)