diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 84d231c3614412..2c920720d2aaec 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -48,6 +48,8 @@ bf01f6ae89a95d8f5572e050facfe311c8c28aaf 7480cd8d37fd71a41ce12b759090051c7e14fb5a c0a526dd97b22f462f49a2a0062420523303769d 9ee713a92c2fe8bf5aac4c2f5b0c4c7e6b475b47 +a925c74bd239398b97112e24f952c377345c5bff +92ce852366a8ba90b28254f4df36fc3f6b3b12eb # Win32: EOL code of batch files 23f9a0d655c4d405bb2397a147a1523436205486 diff --git a/NEWS.md b/NEWS.md index 86ddf609d18d45..1e65ff9bcba20e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -178,7 +178,7 @@ They are still available on rubygems.org and can be installed with ### The following default gems are updated. * RubyGems 4.1.0.beta1 - * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17], [v4.0.18][RubyGems-v4.0.18], [v4.0.19][RubyGems-v4.0.19], [v4.0.20][RubyGems-v4.0.20] + * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17], [v4.0.18][RubyGems-v4.0.18], [v4.0.19][RubyGems-v4.0.19], [v4.0.20][RubyGems-v4.0.20], [v4.1.0.beta1][RubyGems-v4.1.0.beta1] * bundler 4.1.0.beta1 * 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12], [v4.0.13][bundler-v4.0.13], [v4.0.14][bundler-v4.0.14], [v4.0.15][bundler-v4.0.15], [v4.0.16][bundler-v4.0.16], [v4.0.17][bundler-v4.0.17] * erb 6.0.7 @@ -226,7 +226,7 @@ They are still available on rubygems.org and can be installed with * 0.6.2 to [v0.6.3][net-imap-v0.6.3], [v0.6.4][net-imap-v0.6.4], [v0.6.4.1][net-imap-v0.6.4.1], [v0.6.5][net-imap-v0.6.5], [v0.6.6][net-imap-v0.6.6] * rbs 4.2.0 * 3.10.0 to [v3.10.1][rbs-v3.10.1], [v3.10.2][rbs-v3.10.2], [v3.10.3][rbs-v3.10.3], [v3.10.4][rbs-v3.10.4], [v4.0.0.dev.1][rbs-v4.0.0.dev.1], [v4.0.0.dev.2][rbs-v4.0.0.dev.2], [v4.0.0.dev.3][rbs-v4.0.0.dev.3], [v4.0.0.dev.4][rbs-v4.0.0.dev.4], [v4.0.0.dev.5][rbs-v4.0.0.dev.5], [v4.0.0][rbs-v4.0.0], [v4.0.1.dev.1][rbs-v4.0.1.dev.1], [v4.0.1.dev.2][rbs-v4.0.1.dev.2], [v4.0.1][rbs-v4.0.1], [v4.0.2][rbs-v4.0.2], [v4.0.3][rbs-v4.0.3], [v4.1.0.pre.1][rbs-v4.1.0.pre.1], [v4.1.0.pre.2][rbs-v4.1.0.pre.2], [v4.1.0][rbs-v4.1.0], [v4.1.1.pre.1][rbs-v4.1.1.pre.1], [v4.1.1][rbs-v4.1.1], [v4.1.2][rbs-v4.1.2], [v4.1.3][rbs-v4.1.3], [v4.2.0.pre.1][rbs-v4.2.0.pre.1], [v4.2.0][rbs-v4.2.0] -* typeprof 0.33.0 +* typeprof 0.33.1 * mutex_m 0.3.0 * bigdecimal 4.1.2 * 4.0.1 to [v4.1.0][bigdecimal-v4.1.0], [v4.1.1][bigdecimal-v4.1.1], [v4.1.2][bigdecimal-v4.1.2] @@ -448,6 +448,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [RubyGems-v4.0.18]: https://github.com/rubygems/rubygems/releases/tag/v4.0.18 [RubyGems-v4.0.19]: https://github.com/rubygems/rubygems/releases/tag/v4.0.19 [RubyGems-v4.0.20]: https://github.com/rubygems/rubygems/releases/tag/v4.0.20 +[RubyGems-v4.1.0.beta1]: https://github.com/rubygems/rubygems/releases/tag/v4.1.0.beta1 [bundler-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.4 [bundler-v4.0.5]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.5 [bundler-v4.0.6]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.6 diff --git a/class.c b/class.c index 3fc6bf1fd006a5..344048c0310514 100644 --- a/class.c +++ b/class.c @@ -24,6 +24,7 @@ #include "internal/box.h" #include "internal/class.h" #include "internal/eval.h" +#include "internal/gc.h" #include "internal/hash.h" #include "internal/object.h" #include "internal/string.h" @@ -2261,6 +2262,9 @@ rb_mod_descendants(VALUE mod) * * Raises an TypeError if the class is not a singleton class. * + * Raises a Ractor::IsolationError if the attached object is not shareable and + * belongs to another Ractor. + * * class Foo; end * * Foo.singleton_class.attached_object #=> Foo @@ -2277,7 +2281,18 @@ rb_class_attached_object(VALUE klass) rb_raise(rb_eTypeError, "'%"PRIsVALUE"' is not a singleton class", klass); } - return RCLASS_ATTACHED_OBJECT(klass); + const VALUE obj = RCLASS_ATTACHED_OBJECT(klass); + + /* A singleton class is shareable whatever it is attached to, so another Ractor can + * hold one attached to an unshareable object. Returning it would share it. */ + if (rb_objspace_foreign_object_p(obj) && !RB_OBJ_SHAREABLE_P(obj)) { + /* No klass in the message: naming a singleton class inspects the very object we + * must not touch from here. */ + rb_raise(rb_eRactorIsolationError, + "can not get an unshareable attached object from another Ractor"); + } + + return obj; } static void diff --git a/compile.c b/compile.c index fcb884eb440b0f..c539f8e91e4715 100644 --- a/compile.c +++ b/compile.c @@ -1265,6 +1265,21 @@ ELEM_SWAP(LINK_ELEMENT *first, LINK_ELEMENT *second) second->prev = first->prev; first->prev = second; + + if (IS_INSN(first) && IS_INSN(second)) { + INSN *first_insn = (INSN*)first; + INSN *second_insn = (INSN*)second; + + // [Bug #22299] If both instructions are on the same line and the first one carries + // a line event we need to swap the event as well. + if (first_insn->insn_info.line_no == second_insn->insn_info.line_no) { + rb_event_flag_t mask = (RUBY_EVENT_LINE | RUBY_EVENT_COVERAGE_LINE); + rb_event_flag_t first_events = first_insn->insn_info.events & mask; + rb_event_flag_t second_events = second_insn->insn_info.events & mask; + first_insn->insn_info.events = (first_insn->insn_info.events & ~mask) | second_events; + second_insn->insn_info.events = (second_insn->insn_info.events & ~mask) | first_events; + } + } } static LINK_ELEMENT * diff --git a/ext/erb/escape/escape.c b/ext/erb/escape/escape.c index 19d8964823d573..903e832f038749 100644 --- a/ext/erb/escape/escape.c +++ b/ext/erb/escape/escape.c @@ -223,7 +223,7 @@ find_next_neon(search_state *search) #endif // HAVE_SIMD_NEON #ifndef find_next -#define find_next_basic +#define find_next find_next_basic #endif static VALUE diff --git a/ext/erb/escape/extconf.rb b/ext/erb/escape/extconf.rb index f00147b8b9ce99..8e1d482d52b127 100644 --- a/ext/erb/escape/extconf.rb +++ b/ext/erb/escape/extconf.rb @@ -6,24 +6,26 @@ else have_func("rb_ext_ractor_safe", "ruby.h") - case RbConfig::CONFIG['host_cpu'] - when /^(arm|aarch64)/ - # Try to compile a small program using NEON instructions - header, type, init, extra = 'arm_neon.h', 'uint8x16_t', 'vdupq_n_u8(32)', nil - when /^(x86_64|x64)/ - header, type, init, extra = 'x86intrin.h', '__m128i', '_mm_set1_epi8(32)', 'if (__builtin_cpu_supports("sse2")) { printf("OK"); }' - end - if header - if have_header(header) && try_compile(<<~SRC, '-Werror=implicit-function-declaration') - #{cpp_include(header)} - int main(int argc, char **argv) { - #{type} test = #{init}; - #{extra} - if (argc > 100000) printf("%p", &test); - return 0; - } - SRC - $defs.push("-DERB_ENABLE_SIMD") + unless ENV["ERB_DISABLE_SIMD"] + case RbConfig::CONFIG['host_cpu'] + when /^(arm|aarch64)/ + # Try to compile a small program using NEON instructions + header, type, init, extra = 'arm_neon.h', 'uint8x16_t', 'vdupq_n_u8(32)', nil + when /^(x86_64|x64)/ + header, type, init, extra = 'x86intrin.h', '__m128i', '_mm_set1_epi8(32)', 'if (__builtin_cpu_supports("sse2")) { printf("OK"); }' + end + if header + if have_header(header) && try_compile(<<~SRC, '-Werror=implicit-function-declaration') + #{cpp_include(header)} + int main(int argc, char **argv) { + #{type} test = #{init}; + #{extra} + if (argc > 100000) printf("%p", &test); + return 0; + } + SRC + $defs.push("-DERB_ENABLE_SIMD") + end end end diff --git a/gems/bundled_gems b/gems/bundled_gems index 1f6ecf9d426ea0..c514ab06ec0d22 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -17,7 +17,7 @@ net-smtp 0.5.1 https://github.com/ruby/net-smtp matrix 0.4.3 https://github.com/ruby/matrix prime 0.1.4 https://github.com/ruby/prime rbs 4.2.0 https://github.com/ruby/rbs -typeprof 0.33.0 https://github.com/ruby/typeprof +typeprof 0.33.1 https://github.com/ruby/typeprof debug 1.11.1 https://github.com/ruby/debug 6510cfbc7496c55ebbefa437a25c17ca58f7c5eb racc 1.8.1 https://github.com/ruby/racc mutex_m 0.3.0 https://github.com/ruby/mutex_m diff --git a/spec/ruby/core/data/initialize_spec.rb b/spec/ruby/core/data/initialize_spec.rb index 0320ca880c7317..3240093d586b63 100644 --- a/spec/ruby/core/data/initialize_spec.rb +++ b/spec/ruby/core/data/initialize_spec.rb @@ -180,6 +180,19 @@ def initialize(*, **) ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}] end + it "receives the block passed to .new" do + klass = Data.define(:amount) do + def initialize(amount:, &block) + super(amount: block.call(amount)) + end + end + + klass.new(1) { |amount| amount * 2 }.amount.should == 2 + klass.new(amount: 1) { |amount| amount * 2 }.amount.should == 2 + klass[1] { |amount| amount * 2 }.amount.should == 2 + klass[amount: 1] { |amount| amount * 2 }.amount.should == 2 + end + it "accepts positional arguments with empty keyword arguments" do data = DataSpecs::SingleWithOverriddenName.new(42, **{}) diff --git a/spec/ruby/core/encoding/find_spec.rb b/spec/ruby/core/encoding/find_spec.rb index c5356560ebd012..026e041edd2f68 100644 --- a/spec/ruby/core/encoding/find_spec.rb +++ b/spec/ruby/core/encoding/find_spec.rb @@ -53,6 +53,10 @@ def to_str; @encoding_name; end -> { Encoding.find('dh2dh278d') }.should.raise(ArgumentError, 'unknown encoding name - dh2dh278d') end + it "raises an ArgumentError when the name is in a non-ASCII-compatible encoding" do + -> { Encoding.find("utf-8".encode("utf-16be")) }.should.raise(ArgumentError, "invalid encoding name (non ASCII)") + end + # Not sure how to do a better test, since locale depends on weird platform-specific stuff it "supports the 'locale' encoding alias" do enc = Encoding.find('locale') diff --git a/spec/ruby/core/enumerator/size_spec.rb b/spec/ruby/core/enumerator/size_spec.rb index 4b2beffbbe584a..ac8b9339e2fd59 100644 --- a/spec/ruby/core/enumerator/size_spec.rb +++ b/spec/ruby/core/enumerator/size_spec.rb @@ -5,6 +5,10 @@ Enumerator.new(100) {}.size.should == 100 end + it "returns 0 if set size is 0" do + Enumerator.new(0) {}.size.should == 0 + end + it "returns nil if set size is nil" do Enumerator.new(nil) {}.size.should == nil end diff --git a/spec/ruby/core/fiber/current_scheduler_spec.rb b/spec/ruby/core/fiber/current_scheduler_spec.rb new file mode 100644 index 00000000000000..25f6ddeb10a889 --- /dev/null +++ b/spec/ruby/core/fiber/current_scheduler_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/scheduler' + +describe "Fiber.current_scheduler" do + it "returns nil when no scheduler is set" do + Fiber.scheduler.should == nil + Fiber.current_scheduler.should == nil + end + + describe "when a scheduler is set" do + before :each do + @scheduler = FiberSpecs::LoggingScheduler.new + Fiber.set_scheduler(@scheduler) + end + + after :each do + Fiber.set_scheduler(nil) + end + + it "returns nil on the root Fiber, which is blocking" do + Fiber.current_scheduler.should == nil + end + + it "returns the scheduler inside a non-blocking Fiber" do + seen = nil + Fiber.new(blocking: false) { seen = Fiber.current_scheduler }.resume + seen.should.equal?(@scheduler) + end + + it "returns nil inside a blocking Fiber, where Fiber.scheduler still returns the scheduler" do + seen = nil + Fiber.new(blocking: true) { seen = [Fiber.scheduler, Fiber.current_scheduler] }.resume + seen.should == [@scheduler, nil] + end + + it "returns nil inside a blocking Fiber nested in a non-blocking Fiber" do + seen = nil + Fiber.new(blocking: false) do + Fiber.new(blocking: true) { seen = Fiber.current_scheduler }.resume + end.resume + seen.should == nil + end + + it "returns the scheduler inside a non-blocking Fiber nested in a blocking Fiber" do + seen = nil + Fiber.new(blocking: true) do + Fiber.new(blocking: false) { seen = Fiber.current_scheduler }.resume + end.resume + seen.should.equal?(@scheduler) + end + + it "returns nil inside Fiber.blocking in a non-blocking Fiber" do + seen = nil + Fiber.new(blocking: false) { Fiber.blocking { seen = Fiber.current_scheduler } }.resume + seen.should == nil + end + + it "returns the scheduler again after Fiber.blocking returns" do + seen = nil + Fiber.new(blocking: false) do + Fiber.blocking { } + seen = Fiber.current_scheduler + end.resume + seen.should.equal?(@scheduler) + end + + it "returns nil on the root Fiber after a blocking Fiber has finished" do + Fiber.new(blocking: true) { }.resume + Fiber.current_scheduler.should == nil + end + + it "returns nil on the root Fiber after a blocking Fiber has raised" do + fiber = Fiber.new(blocking: true) { raise "from the fiber" } + -> { fiber.resume }.should.raise(RuntimeError) + Fiber.current_scheduler.should == nil + end + + it "returns nil on the root Fiber after a blocking Fiber has been killed" do + fiber = Fiber.new(blocking: true) { Fiber.yield } + fiber.resume + fiber.kill + Fiber.current_scheduler.should == nil + end + end +end diff --git a/spec/ruby/core/fiber/fixtures/scheduler.rb b/spec/ruby/core/fiber/fixtures/scheduler.rb index 16bd2f6b443457..e29a7bac73e525 100644 --- a/spec/ruby/core/fiber/fixtures/scheduler.rb +++ b/spec/ruby/core/fiber/fixtures/scheduler.rb @@ -11,6 +11,11 @@ def block(*args) Fiber.yield end + def fiber(*args, &block) + @events << { event: :fiber, fiber: Fiber.current, args: args } + Fiber.new(blocking: false, &block).tap(&:resume) + end + def io_wait(*args) @events << { event: :io_wait, fiber: Fiber.current, args: args } Fiber.yield diff --git a/spec/ruby/core/fiber/schedule_spec.rb b/spec/ruby/core/fiber/schedule_spec.rb new file mode 100644 index 00000000000000..5eb5b5a9a65418 --- /dev/null +++ b/spec/ruby/core/fiber/schedule_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/scheduler' + +describe "Fiber.schedule" do + describe "when no scheduler is set" do + it "raises a RuntimeError" do + Fiber.scheduler.should == nil + + -> { + Fiber.schedule { } + }.should.raise(RuntimeError) + end + end + + describe "when a scheduler is set" do + before :each do + @scheduler = FiberSpecs::LoggingScheduler.new + Fiber.set_scheduler(@scheduler) + end + + after :each do + Fiber.set_scheduler(nil) + end + + it "calls the scheduler's #fiber hook" do + Fiber.schedule { } + @scheduler.events.map { |event| event[:event] }.should == [:fiber] + end + + it "returns the Fiber which runs the block" do + scheduled = nil + fiber = Fiber.schedule { scheduled = Fiber.current } + fiber.should.equal?(scheduled) + end + + it "can be called from inside a non-blocking Fiber" do + inner = nil + + outer = Fiber.new(blocking: false) do + inner = Fiber.schedule { } + end + outer.resume + + inner.should.is_a?(Fiber) + end + + it "uses the scheduler of the Thread owning the Fiber it is called from" do + seen = nil + + outer = Fiber.new(blocking: false) do + Fiber.schedule { seen = Fiber.scheduler } + end + outer.resume + + seen.should.equal?(@scheduler) + end + + it "runs the block in a Fiber which sees the scheduler as its current scheduler" do + seen = nil + + outer = Fiber.new(blocking: false) do + Fiber.schedule { seen = Fiber.current_scheduler } + end + outer.resume + + seen.should.equal?(@scheduler) + end + end +end diff --git a/spec/ruby/core/file/open_spec.rb b/spec/ruby/core/file/open_spec.rb index 212d1b6d3a6473..ca16e5476cdd94 100644 --- a/spec/ruby/core/file/open_spec.rb +++ b/spec/ruby/core/file/open_spec.rb @@ -386,7 +386,7 @@ File.should.exist?(@file) end - it "opens a file when use File::WRONLY|File::APPEND mode" do + it "opens a file in the read-write append mode when given mode File::RDWR|File::APPEND" do File.open(@file, File::WRONLY) do |f| f.puts("hello file") end @@ -399,6 +399,21 @@ end end + it "opens a file in the append mode when given mode 'a'" do + File.write(@file, "hello") + File.open(@file, "a") { |f| f.write(" world") } + File.read(@file).should == "hello world" + end + + it "opens a file in the read-write append mode when given mode 'a+'" do + File.write(@file, "hello") + File.open(@file, "a+") do |f| + f.read.should == "hello" + f.write(" world") + end + File.read(@file).should == "hello world" + end + it "raises an IOError if the file exists when open with File::RDONLY|File::APPEND" do -> { File.open(@file, File::RDONLY|File::APPEND) do |f| diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb index d6ea3ce8d36d2b..229d46f93b00a3 100644 --- a/spec/ruby/core/io/buffer/resize_spec.rb +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -115,15 +115,15 @@ @buffer.size.should == 1 end - it "preserves read-only access when the allocation is replaced" do - @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED | IO::Buffer::READONLY) - @buffer.resize(16) + ruby_version_is "4.1" do + it "preserves read-only access when the allocation is replaced" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED | IO::Buffer::READONLY) + @buffer.resize(16) - @buffer.should.readonly? - -> { @buffer.set_string("test") }.should.raise(IO::Buffer::AccessError, "Buffer is not writable!") - end + @buffer.should.readonly? + -> { @buffer.set_string("test") }.should.raise(IO::Buffer::AccessError, "Buffer is not writable!") + end - ruby_version_is "4.1" do it "raises FrozenError without resizing a frozen buffer" do buffer = IO::Buffer.new(4) buffer.set_string("test") diff --git a/spec/ruby/core/io/buffer/write_spec.rb b/spec/ruby/core/io/buffer/write_spec.rb new file mode 100644 index 00000000000000..9d8f4b5d9026f0 --- /dev/null +++ b/spec/ruby/core/io/buffer/write_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#write" do + before :each do + @path = tmp("io_buffer_write.txt") + + @file = File.open(@path, "wb+") + @buffer = IO::Buffer.new(5) + @buffer.set_string("Hello") + end + + after :each do + @buffer&.free + @buffer = nil + @file&.close + @file = nil + rm_r @path + end + + it "writes the whole buffer when no length is given" do + @buffer.write(@file).should == 5 + + @file.rewind + @file.read.should == "Hello" + end + + ruby_version_is ""..."4.1" do + it "writes only the given length, starting at the given offset" do + @buffer.write(@file, 4, 1).should == 4 + + @file.rewind + @file.read.should == "ello" + end + end + ruby_version_is "4.1" do + it "writes only the given length, starting at the given offset" do + @buffer.write(@file, 1, 4).should == 4 + + @file.rewind + @file.read.should == "ello" + end + end + + ruby_version_is ""..."4.1" do + it "writes as much as fits in the buffer when length is 0" do + @buffer.write(@file, 0).should == 5 + + @file.rewind + @file.read.should == "Hello" + end + end + ruby_version_is "4.1" do + it "writes 0 bytes when length is 0" do + @buffer.write(@file, 0, 0).should == 0 + + @file.rewind + @file.read.should == "" + end + end + + ruby_version_is ""..."4.1" do + it "writes from offset to the end of the buffer when length is 0 and offset is given" do + @buffer.write(@file, 0, 1).should == 4 + + @file.rewind + @file.read.should == "ello" + end + end + ruby_version_is "4.1" do + it "writes 0 bytes when length is 0 and offset is given" do + @buffer.write(@file, 1, 0).should == 0 + + @file.rewind + @file.read.should == "" + end + end +end diff --git a/spec/ruby/core/io/external_encoding_spec.rb b/spec/ruby/core/io/external_encoding_spec.rb index 72d246cc2bdb9c..57aa5b551b6a7f 100644 --- a/spec/ruby/core/io/external_encoding_spec.rb +++ b/spec/ruby/core/io/external_encoding_spec.rb @@ -118,6 +118,12 @@ @io.external_encoding.should.equal?(Encoding::IBM437) end + it "returns the value of Encoding.default_external when the instance was created if only the internal encoding is set" do + @io = new_io @name, mode: "r", internal_encoding: "utf-8" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should.equal?(Encoding::IBM866) + end + it "returns the external encoding specified when the instance was created" do @io = new_io @name, "r:utf-8" Encoding.default_external = Encoding::IBM437 diff --git a/spec/ruby/core/io/popen_spec.rb b/spec/ruby/core/io/popen_spec.rb index dafb8f08696ecb..8bf1c4be1f21ba 100644 --- a/spec/ruby/core/io/popen_spec.rb +++ b/spec/ruby/core/io/popen_spec.rb @@ -29,6 +29,14 @@ @io.read.should == "foo\n" end + platform_is_not :windows do + it "redirects the child's STDIN from the parent's STDOUT" do + skip "requires STDOUT to be a terminal device" unless STDOUT.tty? + + IO.popen([*ruby_exe, "-e", "print STDIN.tty?", in: STDOUT], &:read).should == "true" + end + end + it "raises IOError when writing a read-only pipe" do @io = IO.popen('echo foo', "r") -> { @io.write('bar') }.should.raise(IOError) diff --git a/spec/ruby/core/io/pread_spec.rb b/spec/ruby/core/io/pread_spec.rb index cfb8dc4c68b821..2cc174c6affad9 100644 --- a/spec/ruby/core/io/pread_spec.rb +++ b/spec/ruby/core/io/pread_spec.rb @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "IO#pread" do before :each do @@ -131,8 +132,19 @@ end it "raises IOError when file is closed" do - file = File.open(@fname, "r+") - file.close - -> { file.pread(1, 1) }.should.raise(IOError) + @file.close + -> { @file.pread(1, 1) }.should.raise(IOError) + end + + it "clears the buffer if end-of-file is reached" do + buffer = +"existing content" + -> { @file.pread(1, 10, buffer) }.should.raise(EOFError) + buffer.should.empty? + end + + it "does not modify the buffer if a read error (other than EOF) occurs" do + buffer = +"existing content" + -> { IOSpecs.closed_io.pread(1, 0, buffer) }.should.raise(IOError) + buffer.should == "existing content" end end diff --git a/spec/ruby/core/io/read_nonblock_spec.rb b/spec/ruby/core/io/read_nonblock_spec.rb index bd36b04582e2cf..db73cd829480ec 100644 --- a/spec/ruby/core/io/read_nonblock_spec.rb +++ b/spec/ruby/core/io/read_nonblock_spec.rb @@ -113,7 +113,7 @@ buffer.should == "hello world" end - it "discards the existing buffer content upon error" do + it "discards the existing buffer content upon EOFError" do buffer = +"existing content" @write.close -> { @read.read_nonblock(1, buffer) }.should.raise(EOFError) @@ -149,4 +149,10 @@ buffer.encoding.should == Encoding::ISO_8859_1 end + + it "does not modify the buffer if a read error (other than EOF) occurs" do + buffer = +"existing content" + -> { IOSpecs.closed_io.read_nonblock(1, buffer) }.should.raise(IOError) + buffer.should == "existing content" + end end diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb index 85b3acf81383bb..187f12dfcb3175 100644 --- a/spec/ruby/core/io/read_spec.rb +++ b/spec/ruby/core/io/read_spec.rb @@ -319,6 +319,12 @@ buf.should == '' end + it "does not modify the buffer if a read error occurs" do + buffer = +"existing content" + -> { IOSpecs.closed_io.read(1, buffer) }.should.raise(IOError) + buffer.should == "existing content" + end + it "returns the empty string when there is nothing to read and lenght=0 is given" do @io.read(11) @io.read(0).should == "" diff --git a/spec/ruby/core/io/readpartial_spec.rb b/spec/ruby/core/io/readpartial_spec.rb index d3f5545c8fc0c8..cc472675a38602 100644 --- a/spec/ruby/core/io/readpartial_spec.rb +++ b/spec/ruby/core/io/readpartial_spec.rb @@ -70,13 +70,13 @@ @wr.write("abc") @wr.close @rd.readpartial(10).should == 'abc' - -> { @rd.readpartial(10) }.should.raise(EOFError) + -> { @rd.readpartial(10) }.should.raise(EOFError, "end of file reached") end - it "discards the existing buffer content upon error" do + it "discards the existing buffer content upon EOFError" do buffer = +'hello' @wr.close - -> { @rd.readpartial(1, buffer) }.should.raise(EOFError) + -> { @rd.readpartial(1, buffer) }.should.raise(EOFError, "end of file reached") buffer.should.empty? end @@ -112,4 +112,10 @@ buffer.encoding.should == Encoding::ISO_8859_1 end + + it "does not modify the buffer if a read error (other than EOF) occurs" do + buffer = +"existing content" + -> { IOSpecs.closed_io.readpartial(1, buffer) }.should.raise(IOError) + buffer.should == "existing content" + end end diff --git a/spec/ruby/core/io/reopen_spec.rb b/spec/ruby/core/io/reopen_spec.rb index 758793f0e08628..a1df72ac49237d 100644 --- a/spec/ruby/core/io/reopen_spec.rb +++ b/spec/ruby/core/io/reopen_spec.rb @@ -45,8 +45,8 @@ it "raises an IOError when called on a closed stream with an object" do @io.close obj = mock("io") - obj.should_not_receive(:to_io) - -> { @io.reopen(STDOUT) }.should.raise(IOError) + obj.should_receive(:to_io).and_return(STDOUT) + -> { @io.reopen obj }.should.raise(IOError) end it "raises an IOError if the IO argument is closed" do diff --git a/spec/ruby/core/io/rewind_spec.rb b/spec/ruby/core/io/rewind_spec.rb index 43834ef3071ded..8812dc09d570e8 100644 --- a/spec/ruby/core/io/rewind_spec.rb +++ b/spec/ruby/core/io/rewind_spec.rb @@ -47,6 +47,14 @@ @io.rewind.should == 0 end + ruby_bug "#20919", "" ... "3.4" do + it "clears the character buffer" do + @io.ungetc("a") + @io.rewind + @io.getc.should == "V" + end + end + it "raises IOError on closed stream" do -> { IOSpecs.closed_io.rewind }.should.raise(IOError) end diff --git a/spec/ruby/core/io/seek_spec.rb b/spec/ruby/core/io/seek_spec.rb index 9d42e3a5c727f2..186af7cf8bdce6 100644 --- a/spec/ruby/core/io/seek_spec.rb +++ b/spec/ruby/core/io/seek_spec.rb @@ -88,6 +88,13 @@ @io.seek(1, IO::SEEK_SET) @io.getc.should == "o".encode(Encoding::UTF_16LE) end + + it "clears the character buffer even when ungotten characters exceed consumed characters" do + @io.getc.should == "V" + @io.ungetc("123456") + @io.seek(1, IO::SEEK_SET) + @io.getc.should == "o" + end end platform_is :darwin do diff --git a/spec/ruby/core/io/set_encoding_spec.rb b/spec/ruby/core/io/set_encoding_spec.rb index 27f5a4103c29b3..c0082f03212ddb 100644 --- a/spec/ruby/core/io/set_encoding_spec.rb +++ b/spec/ruby/core/io/set_encoding_spec.rb @@ -313,6 +313,7 @@ it "raises ArgumentError when argument is not ASCII compatible" do -> { @io.set_encoding("utf-8".encode(Encoding::UTF_16BE)) }.should.raise(ArgumentError) + -> { @io.set_encoding("utf-8", "utf-8".encode(Encoding::UTF_16BE)) }.should.raise(ArgumentError) end it "raises TypeError when the first argument is nil and the second is not nil" do @@ -337,6 +338,10 @@ io.set_encoding(Encoding::UTF_16BE) }.should.raise(ArgumentError, "ASCII incompatible encoding needs binmode") + -> { + io.set_encoding("utf-16be") + }.should.raise(ArgumentError, "ASCII incompatible encoding needs binmode") + Encoding.default_external = Encoding::UTF_16BE -> { @@ -393,6 +398,15 @@ -> { @io.set_encoding("utf-8", newline: :lf) }.should.raise(ArgumentError, "newline decorator with binary mode") + -> { + @io.set_encoding("utf-8", universal_newline: true) + }.should.raise(ArgumentError, "newline decorator with binary mode") + -> { + @io.set_encoding("utf-8", crlf_newline: true) + }.should.raise(ArgumentError, "newline decorator with binary mode") + -> { + @io.set_encoding("utf-8", cr_newline: true) + }.should.raise(ArgumentError, "newline decorator with binary mode") end it "sets the internal encoding to Encoding.default_internal when the second argument is nil" do diff --git a/spec/ruby/core/io/sysread_spec.rb b/spec/ruby/core/io/sysread_spec.rb index 4b9aba46a23628..c677fe47210bca 100644 --- a/spec/ruby/core/io/sysread_spec.rb +++ b/spec/ruby/core/io/sysread_spec.rb @@ -103,10 +103,10 @@ buffer.should == "01234567890" end - it "discards the existing buffer content upon error" do + it "discards the existing buffer content upon EOFError" do buffer = +"existing content" @file.seek(0, IO::SEEK_END) - -> { @file.sysread(1, buffer) }.should.raise(EOFError) + -> { @file.sysread(1, buffer) }.should.raise(EOFError, "end of file reached") buffer.should.empty? end @@ -133,7 +133,9 @@ @read.sysread(3).should == "ab" end - it "raises ArgumentError when length is less than 0" do - -> { @read.sysread(-1) }.should.raise(ArgumentError) + it "does not modify the buffer if a read error (other than EOF) occurs" do + buffer = +"existing content" + -> { IOSpecs.closed_io.sysread(1, buffer) }.should.raise(IOError) + buffer.should == "existing content" end end diff --git a/spec/ruby/core/io/tty_spec.rb b/spec/ruby/core/io/tty_spec.rb index e1848a1760a09c..0125e0a6956444 100644 --- a/spec/ruby/core/io/tty_spec.rb +++ b/spec/ruby/core/io/tty_spec.rb @@ -22,4 +22,21 @@ it "raises IOError on closed stream" do -> { IOSpecs.closed_io.tty? }.should.raise(IOError) end + + it "returns false for stdio streams if they are not connected to a terminal" do + skip "requires STDOUT and STDERR to be terminal devices" unless STDOUT.tty? && STDERR.tty? + begin + io = IO.popen(ruby_cmd('print [STDIN.tty?, STDOUT.tty?, STDERR.tty?].inspect'), "r") + io.read.should == "[true, false, true]" + ensure + io&.close + end + + begin + io = IO.popen(ruby_cmd('print [STDIN.tty?, STDOUT.tty?, STDERR.tty?].inspect'), "r+") + io.read.should == "[false, false, true]" + ensure + io&.close + end + end end diff --git a/spec/ruby/core/io/ungetc_spec.rb b/spec/ruby/core/io/ungetc_spec.rb index e027632dc9e3e2..873c8be6d56ec0 100644 --- a/spec/ruby/core/io/ungetc_spec.rb +++ b/spec/ruby/core/io/ungetc_spec.rb @@ -108,6 +108,12 @@ @io.gets.chomp.should == "Aquí Qui è la linea due." end + it "puts correctly back a string longer than the amount of data previously read" do + @io.read(5).should == "Voici" + @io.ungetc("1234567890").should == nil + @io.gets.chomp.should == "1234567890 la ligne une." + end + it "calls #to_str to convert the argument if it is not an Integer" do chars = mock("io ungetc") chars.should_receive(:to_str).and_return("Aquí ") diff --git a/spec/ruby/core/marshal/dump_spec.rb b/spec/ruby/core/marshal/dump_spec.rb index 9bbb7809af3ada..a77852049d2007 100644 --- a/spec/ruby/core/marshal/dump_spec.rb +++ b/spec/ruby/core/marshal/dump_spec.rb @@ -622,6 +622,13 @@ def _dump(level) Marshal.dump(h).should == "\004\bC:\tHash{\x00" end + it "dumps a Hash with compare_by_identity and registers the \"Hash\" symbol in the link table" do + h = {} + h.compare_by_identity + + Marshal.dump([h, :Hash]).should == "\x04\b[\aC:\tHash{\x00;\x00" + end + it "dumps a Hash subclass with compare_by_identity" do h = UserHash.new h.compare_by_identity diff --git a/spec/ruby/core/marshal/fixtures/marshal_data.rb b/spec/ruby/core/marshal/fixtures/marshal_data.rb index c16d9e4bb68205..ca2cc25475a1bc 100644 --- a/spec/ruby/core/marshal/fixtures/marshal_data.rb +++ b/spec/ruby/core/marshal/fixtures/marshal_data.rb @@ -159,6 +159,48 @@ def ==(other) end module MarshalSpec + class UserMarshalWithModuleCheck + def marshal_dump + :data + end + + def marshal_load(data) + ScratchPad.record respond_to?(:meths_method) + end + end + + class UserMarshalWithPayload + attr_accessor :payload + + def initialize(payload) + @payload = payload + end + + def marshal_dump + @payload + end + + def marshal_load(payload) + @payload = payload + end + end + + class UserDefinedWithPayload + attr_accessor :payload + + def initialize(payload) + @payload = payload + end + + def _dump(depth) + Marshal.dump(@payload) + end + + def self._load(str) + new(Marshal.load(str)) + end + end + class UserMarshalDumpWithIvar attr_reader :data diff --git a/spec/ruby/core/marshal/load_spec.rb b/spec/ruby/core/marshal/load_spec.rb index f5a05f8e527406..04068ce5b40d3e 100644 --- a/spec/ruby/core/marshal/load_spec.rb +++ b/spec/ruby/core/marshal/load_spec.rb @@ -314,6 +314,47 @@ object.string.should == "a".encode("utf-32le") end + it "loads an object extended with a module" do + obj = UserDefined.new + dump = "\x04\be:\nMethsu:\x10UserDefined\x12\x04\b[\a:\nstuff;\x00" + dump.should == "\x04\be:\nMeths" + Marshal.dump(obj)[2..-1] + loaded = Marshal.load(dump) + loaded.should.instance_of?(UserDefined) + loaded.singleton_class.ancestors[@num_self_class, 2].should == [Meths, UserDefined] + end + + it "does not leak the extending module to nested objects when loading an extended user-defined _dump/_load object" do + obj = MarshalSpec::UserDefinedWithPayload.new(Object.new) + dump = "\x04\be:\nMethsu:(MarshalSpec::UserDefinedWithPayload\x11\x04\bo:\vObject\x00" + dump.should == "\x04\be:\nMeths" + Marshal.dump(obj)[2..-1] + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.payload.should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended user-defined _dump/_load object and a nested object extended with another module" do + obj = MarshalSpec::UserDefinedWithPayload.new(Object.new.extend(MethsMore)) + dump = "\x04\be:\nMethsu:(MarshalSpec::UserDefinedWithPayload\x1D\x04\be:\x0EMethsMoreo:\vObject\x00" + dump.should == "\x04\be:\nMeths" + Marshal.dump(obj)[2..-1] + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded.payload.should.is_a?(MethsMore) + loaded.payload.should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended user-defined _dump/_load object and a nested user-defined payload extended with another module" do + payload = UserDefined.new + nested_dump = "\x04\be:\x0EMethsMore" + Marshal.dump(payload)[2..-1] + dump = "\x04\be:\nMethsu:(MarshalSpec::UserDefinedWithPayload/\x04\be:\x0EMethsMoreu:\x10UserDefined\x12\x04\b[\a:\nstuff;\x00" + dump.should == "\x04\be:\nMethsu:(MarshalSpec::UserDefinedWithPayload" + [nested_dump.bytesize + 5].pack("C") + nested_dump + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded.payload.should.is_a?(MethsMore) + loaded.payload.should_not.is_a?(Meths) + end + describe "that returns an immediate value" do it "loads an array containing an instance of the object, followed by multiple instances of another object" do str = "string" @@ -476,6 +517,26 @@ new_obj_ancestors = class << new_obj; ancestors[1..-1]; end obj_ancestors.should == new_obj_ancestors end + + it "does not leak the extending module to nested elements when loading an extended Array" do + obj = [Object.new].extend(Meths) + dump = "\x04\be:\nMeths[\x06o:\vObject\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded[0].should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended Array and a nested element extended with another module" do + obj = [Object.new.extend(MethsMore)].extend(Meths) + dump = "\x04\be:\nMeths[\x06e:\x0EMethsMoreo:\vObject\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded[0].should.is_a?(MethsMore) + loaded[0].should_not.is_a?(Meths) + end end describe "for a Hash" do @@ -501,6 +562,26 @@ new_obj_metaclass_ancestors[@num_self_class+1].should == Hash end + it "does not leak the extending module to nested objects when loading an extended Hash" do + obj = { key: Object.new }.extend(Meths) + dump = "\x04\be:\nMeths{\x06:\bkeyo:\vObject\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded[:key].should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended Hash and a nested object extended with another module" do + obj = { key: Object.new.extend(MethsMore) }.extend(Meths) + dump = "\x04\be:\nMeths{\x06:\bkeye:\x0EMethsMoreo:\vObject\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded[:key].should.is_a?(MethsMore) + loaded[:key].should_not.is_a?(Meths) + end + it "preserves hash ivars when hash contains a string having ivar" do s = +'string' s.instance_variable_set :@string_ivar, 'string ivar' @@ -678,6 +759,51 @@ def io.binmode; raise "binmode"; end result.should == str end + it "loads an extended String" do + dump = "\x04\be:\nMeths\"\bfoo" + loaded = Marshal.load(dump) + loaded.should == "foo" + loaded.singleton_class.ancestors[@num_self_class, 2].should == [Meths, String] + end + + it "loads an extended String with instance variables" do + dump = "\x04\bIe:\nMeths\"\bfoo\x06:\a@xT" + loaded = Marshal.load(dump) + loaded.should == "foo" + loaded.instance_variable_get(:@x).should == true + loaded.singleton_class.ancestors[@num_self_class, 2].should == [Meths, String] + end + + it "loads an extended String subclass" do + dump = "\x04\be:\nMethsC:\x0FUserString\"\bfoo" + loaded = Marshal.load(dump) + loaded.should.instance_of?(UserString) + loaded.should == "foo" + loaded.singleton_class.ancestors[@num_self_class, 3].should == [Meths, UserString, String] + end + + it "does not leak the extending module to instance variables when loading an extended String" do + obj = "hello".dup.force_encoding("BINARY").extend(Meths) + obj.instance_variable_set(:@ivar, Object.new) + dump = "\x04\bIe:\nMeths\"\nhello\x06:\n@ivaro:\vObject\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.instance_variable_get(:@ivar).should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended String and an instance variable extended with another module" do + obj = "hello".dup.force_encoding("BINARY").extend(Meths) + obj.instance_variable_set(:@ivar, Object.new.extend(MethsMore)) + dump = "\x04\bIe:\nMeths\"\nhello\x06:\n@ivare:\x0EMethsMoreo:\vObject\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded.instance_variable_get(:@ivar).should.is_a?(MethsMore) + loaded.instance_variable_get(:@ivar).should_not.is_a?(Meths) + end + it "raises ArgumentError when end of byte sequence reached before string characters end" do Marshal.dump("hello").should == "\x04\b\"\nhello" @@ -701,6 +827,26 @@ def io.binmode; raise "binmode"; end Struct.send(:remove_const, :Extended) end + it "does not leak the extending module to member objects when loading an extended Struct" do + obj = Struct::Useful.new(Object.new, nil).extend(Meths) + dump = "\x04\be:\nMethsS:\x13Struct::Useful\a:\x06ao:\vObject\x00:\x06b0" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.a.should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended Struct and a member object extended with another module" do + obj = Struct::Useful.new(Object.new.extend(MethsMore), nil).extend(Meths) + dump = "\x04\be:\nMethsS:\x13Struct::Useful\a:\x06ae:\x0EMethsMoreo:\vObject\x00:\x06b0" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded.a.should.is_a?(MethsMore) + loaded.a.should_not.is_a?(Meths) + end + it "loads a struct having ivar" do obj = Struct.new("Thick").new obj.instance_variable_set(:@foo, 5) @@ -820,6 +966,38 @@ def io.binmode; raise "binmode"; end new_obj_metaclass_ancestors[@num_self_class, 2].should == [Meths, Object] end + it "does not leak the extending module to instance variables when loading an extended Object" do + obj = Object.new.extend(Meths) + obj.instance_variable_set(:@ivar, Object.new) + dump = "\x04\be:\nMethso:\vObject\x06:\n@ivaro;\x06\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.instance_variable_get(:@ivar).should_not.is_a?(Meths) + end + + it "does not leak the extending module to a user-marshaled instance variable when loading an extended Object" do + obj = Object.new.extend(Meths) + obj.instance_variable_set(:@ivar, UserMarshal.new) + dump = "\x04\be:\nMethso:\vObject\x06:\n@ivarU:\x10UserMarshal:\tdata" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.instance_variable_get(:@ivar).should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended Object and an instance variable extended with another module" do + obj = Object.new.extend(Meths) + obj.instance_variable_set(:@ivar, Object.new.extend(MethsMore)) + dump = "\x04\be:\nMethso:\vObject\x06:\n@ivare:\x0EMethsMoreo;\x06\x00" + dump.should == Marshal.dump(obj) + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded.instance_variable_get(:@ivar).should.is_a?(MethsMore) + loaded.instance_variable_get(:@ivar).should_not.is_a?(Meths) + end + it "loads an object having ivar" do s = 'hi' arr = [:so, :so, s, s] @@ -865,6 +1043,60 @@ def io.binmode; raise "binmode"; end reloaded = Marshal.load(dump) reloaded.should == value end + + it "loads a user-marshaled object extended with a module" do + obj = UserMarshal.new + obj.data = :data + dump = "\x04\be:\nMethsU:\x10UserMarshal:\tdata" + dump.should == "\x04\be:\nMeths" + Marshal.dump(obj)[2..-1] + loaded = Marshal.load(dump) + loaded.should.instance_of?(UserMarshal) + loaded.data.should == :data + loaded.singleton_class.ancestors[@num_self_class, 2].should == [Meths, UserMarshal] + end + + it "extends the module before calling #marshal_load on a user-marshaled object" do + obj = MarshalSpec::UserMarshalWithModuleCheck.new + dump = "\x04\be:\nMethsU:,MarshalSpec::UserMarshalWithModuleCheck:\tdata" + dump.should == "\x04\be:\nMeths" + Marshal.dump(obj)[2..-1] + ScratchPad.record nil + loaded = Marshal.load(dump) + ScratchPad.recorded.should == true + loaded.should.is_a?(Meths) + end + + it "does not leak the extending module to nested payload objects when loading an extended user-marshaled object" do + obj = MarshalSpec::UserMarshalWithPayload.new(Object.new) + dump = "\x04\be:\nMethsU:(MarshalSpec::UserMarshalWithPayloado:\vObject\x00" + dump.should == "\x04\be:\nMeths" + Marshal.dump(obj)[2..-1] + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.payload.should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended user-marshaled object and a nested payload object extended with another module" do + obj = MarshalSpec::UserMarshalWithPayload.new(Object.new.extend(MethsMore)) + dump = "\x04\be:\nMethsU:(MarshalSpec::UserMarshalWithPayloade:\x0EMethsMoreo:\vObject\x00" + dump.should == "\x04\be:\nMeths" + Marshal.dump(obj)[2..-1] + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded.payload.should.is_a?(MethsMore) + loaded.payload.should_not.is_a?(Meths) + end + + it "isolates extending modules between an extended user-marshaled object and a nested user-marshaled payload extended with another module" do + payload = UserMarshal.new + payload.data = :data + nested_dump = "\x04\be:\x0EMethsMore" + Marshal.dump(payload)[2..-1] + dump = "\x04\be:\nMethsU:(MarshalSpec::UserMarshalWithPayloade:\x0EMethsMoreU:\x10UserMarshal:\tdata" + dump.should == "\x04\be:\nMethsU:(MarshalSpec::UserMarshalWithPayload" + nested_dump[2..-1] + loaded = Marshal.load(dump) + loaded.should.is_a?(Meths) + loaded.should_not.is_a?(MethsMore) + loaded.payload.should.is_a?(MethsMore) + loaded.payload.should_not.is_a?(Meths) + end end describe "for a user object" do @@ -1268,6 +1500,26 @@ def _dump_data end end + describe "for an object extended with a module" do + it "loads an object extended with multiple modules preserving the module ancestor order" do + dump = "\x04\be:\nMethse:\x0EMethsMoreo:\vObject\x00" + loaded = Marshal.load(dump) + loaded.singleton_class.ancestors[@num_self_class, 3].should == [Meths, MethsMore, Object] + end + + it "raises ArgumentError when the extending module does not exist" do + -> { + Marshal.load("\x04\be:\x16NonExistentModuleo:\vObject\x00") + }.should.raise(ArgumentError, /undefined class\/module/) + end + + it "raises ArgumentError when the extending constant is a Class instead of a Module" do + -> { + Marshal.load("\x04\be:\vStringo:\vObject\x00") + }.should.raise(ArgumentError) + end + end + describe "when a class does not exist in the namespace" do before :each do NamespaceTest.send(:const_set, :SameName, Class.new) diff --git a/spec/ruby/core/method/shared/source_range.rb b/spec/ruby/core/method/shared/source_range.rb index a85f7120ecb02d..c4027630ad9dd4 100644 --- a/spec/ruby/core/method/shared/source_range.rb +++ b/spec/ruby/core/method/shared/source_range.rb @@ -130,4 +130,9 @@ def my_method_source_range range.start_line.should == 100 end + + it "returns nil if the method is not defined in Ruby" do + method = @object.call(42.method(:__id__)) + method.source_range.should == nil + end end diff --git a/spec/ruby/core/method/shared/syntax_tree.rb b/spec/ruby/core/method/shared/syntax_tree.rb new file mode 100644 index 00000000000000..558be4c3d86ea2 --- /dev/null +++ b/spec/ruby/core/method/shared/syntax_tree.rb @@ -0,0 +1,36 @@ +require_relative '../../../fixtures/source_range_helpers' + +describe :method_syntax_tree, shared: true do + it "returns a DefNode for a def method" do + def def_method + end + def_method_line = __LINE__ - 2 + + node = @object.call(method(:def_method)).syntax_tree + node.start_line.should == def_method_line + node.should.is_a?(Prism::DefNode) + end + + it "currently returns a CallNode for a define_method method" do + define_singleton_method :define_method_method do + end + define_method_method_line = __LINE__ - 2 + + node = @object.call(method(:define_method_method)).syntax_tree + node.start_line.should == define_method_method_line + node.should.is_a?(Prism::CallNode) + end + + it "currently returns a CallNode for a define_method(name, &proc) method" do + def return_block(&b) + b + end + body = return_block { 42 } + define_singleton_method(:define_method_method_proc, &body) + define_method_method_proc_line = __LINE__ - 2 + + node = @object.call(method(:define_method_method_proc)).syntax_tree + node.start_line.should == define_method_method_proc_line + node.should.is_a?(Prism::CallNode) + end +end diff --git a/spec/ruby/core/method/syntax_tree_spec.rb b/spec/ruby/core/method/syntax_tree_spec.rb new file mode 100644 index 00000000000000..04dfd0f30b88db --- /dev/null +++ b/spec/ruby/core/method/syntax_tree_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'shared/syntax_tree' + +ruby_version_is "4.1" do + describe "Method#syntax_tree" do + before :each do + @object = -> method { method } + + skip "parse.y" if method(:it).syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + end + + it_behaves_like :method_syntax_tree, :syntax_tree + end +end diff --git a/spec/ruby/core/module/alias_method_spec.rb b/spec/ruby/core/module/alias_method_spec.rb index 852879cc8a9c86..57b7eea48e9bde 100644 --- a/spec/ruby/core/module/alias_method_spec.rb +++ b/spec/ruby/core/module/alias_method_spec.rb @@ -77,6 +77,19 @@ def uno_refined_method @class.make_alias "cinq", name end + it "raises an EncodingError for a String name containing invalid bytes" do + invalid_utf8 = (+"\xFF").force_encoding(Encoding::UTF_8) + -> { + @class.make_alias invalid_utf8, :public_one + }.should.raise(EncodingError, 'invalid symbol in encoding UTF-8 :"\xFF"') + + name = Object.new + name.define_singleton_method(:to_str) { invalid_utf8 } + -> { + @class.make_alias name, :public_one + }.should.raise(EncodingError, 'invalid symbol in encoding UTF-8 :"\xFF"') + end + it "raises a TypeError when the given name can't be converted using to_str" do -> { @class.make_alias mock('x'), :public_one }.should.raise(TypeError) end diff --git a/spec/ruby/core/proc/syntax_tree_spec.rb b/spec/ruby/core/proc/syntax_tree_spec.rb new file mode 100644 index 00000000000000..9ad89ee44bad5b --- /dev/null +++ b/spec/ruby/core/proc/syntax_tree_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' + +ruby_version_is "4.1" do + describe "Proc#syntax_tree" do + before :each do + skip "parse.y" if proc {}.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + end + + def return_block(&b) + b + end + + it "currently returns a CallNode and not a BlockNode for a block" do + node = proc { 42 }.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::CallNode) + + node = lambda { 42 }.syntax_tree # rubocop:disable Style/Lambda + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::CallNode) + + node = return_block { 42 }.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::CallNode) + end + + it "currently returns a CallNode and not a BlockNode for a block with rescue" do + # Interesting because the BlockNode and the BeginNode are fully overlapping + node = proc do; rescue; end.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::CallNode) + end + + it "returns a LambdaNode for a stabby lambda" do + node = -> { 42 }.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::LambdaNode) + end + + it "does not return implicit parameter nodes" do + # Interesting because the BlockNode and the ItParametersNode/NumberedParametersNode are fully overlapping + node = -> { it }.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::LambdaNode) + + node = -> { _1 }.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::LambdaNode) + + node = proc { it }.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::CallNode) + + node = proc { _1 }.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::CallNode) + end + + it "returns a ForNode for a for-loop block" do + iter = Object.new + def iter.each(&block) + block.call(block) + end + for b in iter + node = b.syntax_tree + end + + node.start_line.should == __LINE__ - 4 + node.should.is_a?(Prism::ForNode) + end + end +end diff --git a/spec/ruby/core/process/exec_spec.rb b/spec/ruby/core/process/exec_spec.rb index da9b4d38f9096b..0aca2800fe6f3c 100644 --- a/spec/ruby/core/process/exec_spec.rb +++ b/spec/ruby/core/process/exec_spec.rb @@ -240,20 +240,23 @@ end describe "options validation" do + # Use a non-existent command to prevent unexpected execve() calls and + # provide clearer failures if options aren't validated. + it "raises an ArgumentError if :unsetenv_others option is not a boolean or nil" do - -> { Process.exec("true", unsetenv_others: 1) }.should.raise(ArgumentError, /expected true or false/) - -> { Process.exec("true", unsetenv_others: "true") }.should.raise(ArgumentError, /expected true or false/) + -> { Process.exec("should_raise_before_and_not_run", unsetenv_others: 1) }.should.raise(ArgumentError, /expected true or false/) + -> { Process.exec("should_raise_before_and_not_run", unsetenv_others: "true") }.should.raise(ArgumentError, /expected true or false/) end it "raises an ArgumentError if :close_others option is not a boolean or nil" do - -> { Process.exec("true", close_others: 1) }.should.raise(ArgumentError, /expected true or false/) - -> { Process.exec("true", close_others: "true") }.should.raise(ArgumentError, /expected true or false/) + -> { Process.exec("should_raise_before_and_not_run", close_others: 1) }.should.raise(ArgumentError, /expected true or false/) + -> { Process.exec("should_raise_before_and_not_run", close_others: "true") }.should.raise(ArgumentError, /expected true or false/) end platform_is :windows do it "raises an ArgumentError if :new_pgroup option is not a boolean or nil" do - -> { Process.exec("true", new_pgroup: 1) }.should.raise(ArgumentError, /expected true or false/) - -> { Process.exec("true", new_pgroup: "true") }.should.raise(ArgumentError, /expected true or false/) + -> { Process.exec("should_raise_before_and_not_run", new_pgroup: 1) }.should.raise(ArgumentError, /expected true or false/) + -> { Process.exec("should_raise_before_and_not_run", new_pgroup: "true") }.should.raise(ArgumentError, /expected true or false/) end end end diff --git a/spec/ruby/core/set/exclusion_spec.rb b/spec/ruby/core/set/exclusion_spec.rb index 5457e6e3deda8a..6e59bc8dacf8fc 100644 --- a/spec/ruby/core/set/exclusion_spec.rb +++ b/spec/ruby/core/set/exclusion_spec.rb @@ -10,9 +10,13 @@ (@set ^ [3, 4, 5]).should == Set[1, 2, 5] end + it "correctly excludes elements that appear multiple times in the passed Enumerable" do + (@set ^ [3, 4, 5, 5]).should == Set[1, 2, 5] + end + it "raises an ArgumentError when passed a non-Enumerable" do - -> { @set ^ 3 }.should.raise(ArgumentError) - -> { @set ^ Object.new }.should.raise(ArgumentError) + -> { @set ^ 3 }.should.raise(ArgumentError, "value must be enumerable") + -> { @set ^ Object.new }.should.raise(ArgumentError, "value must be enumerable") end ruby_version_is ""..."4.0" do diff --git a/spec/ruby/core/string/count_spec.rb b/spec/ruby/core/string/count_spec.rb index fd127c6ff21b5f..a0536517a8622e 100644 --- a/spec/ruby/core/string/count_spec.rb +++ b/spec/ruby/core/string/count_spec.rb @@ -73,20 +73,26 @@ "abcde".count("^ac-e").should == 1 end - it "raises if the given sequences are invalid" do + it "raises an ArgumentError when the sequence is invalid" do s = "hel-[()]-lo012^" -> { s.count("h-e") }.should.raise(ArgumentError) -> { s.count("^h-e") }.should.raise(ArgumentError) end - it 'returns the number of occurrences of a multi-byte character' do + it "counts multibyte characters" do str = "\u{2605}" str.count(str).should == 1 "asd#{str}zzz#{str}ggg".count(str).should == 2 end - it "calls #to_str to convert each set arg to a String" do + it "respects backslash for escaping" do + "a-b".count("a\\-b").should == 3 + "^".count("\\^").should == 1 + "\\".count("\\\\").should == 1 + end + + it "tries to convert each argument to a string using to_str" do other_string = mock('lo') other_string.should_receive(:to_str).and_return("lo") @@ -97,9 +103,14 @@ s.count(other_string, other_string2).should == s.count("o") end - it "raises a TypeError when a set arg can't be converted to a string" do + it "raises a TypeError when an argument can't be converted to a string" do -> { "hello world".count(100) }.should.raise(TypeError) -> { "hello world".count([]) }.should.raise(TypeError) -> { "hello world".count(mock('x')) }.should.raise(TypeError) end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".count("e".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").count("e") }.should.raise(Encoding::CompatibilityError) + end end diff --git a/spec/ruby/core/string/delete_spec.rb b/spec/ruby/core/string/delete_spec.rb index adb5150cff0d73..e5a4d2b694987e 100644 --- a/spec/ruby/core/string/delete_spec.rb +++ b/spec/ruby/core/string/delete_spec.rb @@ -54,12 +54,15 @@ '哥哥我倒'.delete('哥').should == "我倒" end - it "respects backslash for escaping a -" do + it "respects backslash for escaping" do 'Non-Authoritative Information'.delete(' \-\'').should == 'NonAuthoritativeInformation' + "a-b".delete("a\\-b").should == "" + "^".delete("\\^").should == "" + "\\".delete("\\\\").should == "" end - it "raises if the given ranges are invalid" do + it "raises an ArgumentError when the sequence is invalid" do not_supported_on :opal do xFF = [0xFF].pack('C') range = "\x00 - #{xFF}".force_encoding('utf-8') @@ -69,7 +72,7 @@ -> { "hello".delete("^h-e") }.should.raise(ArgumentError) end - it "tries to convert each set arg to a string using to_str" do + it "tries to convert each argument to a string using to_str" do other_string = mock('lo') other_string.should_receive(:to_str).and_return("lo") @@ -79,7 +82,7 @@ "hello world".delete(other_string, other_string2).should == "hell wrld" end - it "raises a TypeError when one set arg can't be converted to a string" do + it "raises a TypeError when an argument can't be converted to a string" do -> { "hello world".delete(100) }.should.raise(TypeError) -> { "hello world".delete([]) }.should.raise(TypeError) -> { "hello world".delete(mock('x')) }.should.raise(TypeError) @@ -92,6 +95,11 @@ it "returns a String in the same encoding as self" do "hello".encode("US-ASCII").delete("lo").encoding.should == Encoding::US_ASCII end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".delete("e".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").delete("e") }.should.raise(Encoding::CompatibilityError) + end end describe "String#delete!" do diff --git a/spec/ruby/core/string/lstrip_spec.rb b/spec/ruby/core/string/lstrip_spec.rb index 5896f8d7da6bc1..916032eb1fa3b7 100644 --- a/spec/ruby/core/string/lstrip_spec.rb +++ b/spec/ruby/core/string/lstrip_spec.rb @@ -25,6 +25,74 @@ "\x00hello".lstrip.should == "hello" "\000 \000hello\000 \000".lstrip.should == "hello\000 \000" end + + ruby_version_is "4.0" do + context "when given character selectors arguments" do + it "removes leading characters in the intersection of sets removed" do + " hello ".lstrip(" ").should == "hello " + "llo".lstrip("lo", "l").should == "o" + "hello".lstrip("ho", "h").should == "ello" + "hell yeah".lstrip("").should == "hell yeah" + end + + it "negates sets starting with ^" do + "ello".lstrip("aeiou", "^e").should == "ello" + "hello".lstrip("^o").should == "o" + end + + it "removes leading characters in a sequence" do + "hello".lstrip("e-h").should == "llo" + "hel-lo".lstrip("h-").should == "el-lo" + "abcdefgh".lstrip("a-ce-fh").should == "defgh" + "abcde".lstrip("ac-e").should == "bcde" + end + + it "removes leading multibyte characters" do + "四月".lstrip("四").should == "月" + "哥哥我倒".lstrip("哥").should == "我倒" + end + + it "respects backslash for escaping" do + "a-b".lstrip("a\\-b").should == "" + "^".lstrip("\\^").should == "" + "\\".lstrip("\\\\").should == "" + end + + it "raises an ArgumentError when the sequence is invalid" do + -> { "hello".lstrip("h-e") }.should.raise(ArgumentError) + -> { "hello".lstrip("^h-e") }.should.raise(ArgumentError) + end + + it "tries to convert each argument to a string using to_str" do + other_string = mock('h') + other_string.should_receive(:to_str).and_return("h") + + other_string2 = mock('he') + other_string2.should_receive(:to_str).and_return("he") + + "hello world".lstrip(other_string, other_string2).should == "ello world" + end + + it "raises a TypeError when an argument can't be converted to a string" do + -> { "hello world".lstrip(100) }.should.raise(TypeError) + -> { "hello world".lstrip([]) }.should.raise(TypeError) + -> { "hello world".lstrip(mock('x')) }.should.raise(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("oh no!!!").lstrip("o").should.instance_of?(String) + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").lstrip("h").encoding.should == Encoding::US_ASCII + end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".lstrip("e".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").lstrip("e") }.should.raise(Encoding::CompatibilityError) + end + end + end end describe "String#lstrip!" do diff --git a/spec/ruby/core/string/rstrip_spec.rb b/spec/ruby/core/string/rstrip_spec.rb index 1638ea375d9584..ed03a5698be259 100644 --- a/spec/ruby/core/string/rstrip_spec.rb +++ b/spec/ruby/core/string/rstrip_spec.rb @@ -25,6 +25,73 @@ it "returns a copy of self with all trailing whitespace and NULL bytes removed" do "\x00 \x00hello\x00 \x00".rstrip.should == "\x00 \x00hello" end + + ruby_version_is "4.0" do + context "when given character selectors arguments" do + it "removes trailing characters in the intersection of sets removed" do + " hello ".rstrip(" ").should == " hello" + "llo".rstrip("o", "lo").should == "ll" + "hell yeah".rstrip("").should == "hell yeah" + end + + it "negates sets starting with ^" do + "ello".rstrip("aeiou", "^o").should == "ello" + "hello".rstrip("^h").should == "h" + end + + it "removes trailing characters in a sequence" do + "hello".rstrip("l-o").should == "he" + "hel-lo".rstrip("h-").should == "hel-lo" + "abcdefgh".rstrip("a-ce-fh").should == "abcdefg" + "abcde".rstrip("ac-e").should == "ab" + end + + it "removes trailing multibyte characters" do + "四月".rstrip("月").should == "四" + "哥哥我倒".rstrip("倒").should == "哥哥我" + end + + it "respects backslash for escaping" do + "a-b".rstrip("a\\-b").should == "" + "^".rstrip("\\^").should == "" + "\\".rstrip("\\\\").should == "" + end + + it "raises an ArgumentError when the sequence is invalid" do + -> { "hello".rstrip("h-e") }.should.raise(ArgumentError) + -> { "hello".rstrip("^h-e") }.should.raise(ArgumentError) + end + + it "tries to convert each argument to a string using to_str" do + other_string = mock('d') + other_string.should_receive(:to_str).and_return("d") + + other_string2 = mock('ld') + other_string2.should_receive(:to_str).and_return("ld") + + "hello world".rstrip(other_string, other_string2).should == "hello worl" + end + + it "raises a TypeError when an argument can't be converted to a string" do + -> { "hello world".rstrip(100) }.should.raise(TypeError) + -> { "hello world".rstrip([]) }.should.raise(TypeError) + -> { "hello world".rstrip(mock('x')) }.should.raise(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("oh no!!!").rstrip("!").should.instance_of?(String) + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").rstrip("o").encoding.should == Encoding::US_ASCII + end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".rstrip("e".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").rstrip("e") }.should.raise(Encoding::CompatibilityError) + end + end + end end describe "String#rstrip!" do diff --git a/spec/ruby/core/string/squeeze_spec.rb b/spec/ruby/core/string/squeeze_spec.rb index 52b6e1eed44e5a..a9b22aa87c5595 100644 --- a/spec/ruby/core/string/squeeze_spec.rb +++ b/spec/ruby/core/string/squeeze_spec.rb @@ -13,6 +13,7 @@ it "only squeezes chars that are in the intersection of all sets given" do "woot squeeze cheese".squeeze("eost", "queo").should == "wot squeze chese" " now is the".squeeze(" ").should == " now is the" + "hello".squeeze("").should == "hello" end it "negates sets starting with ^" do @@ -49,13 +50,13 @@ "AABBCCaabbcc[[]]".squeeze("A-a").should == "ABCabbcc[]" end - it "raises an ArgumentError when the parameter is out of sequence" do + it "raises an ArgumentError when the sequence is invalid" do s = "--subbookkeeper--" -> { s.squeeze("e-b") }.should.raise(ArgumentError) -> { s.squeeze("^e-b") }.should.raise(ArgumentError) end - it "tries to convert each set arg to a string using to_str" do + it "tries to convert each argument to a string using to_str" do other_string = mock('lo') other_string.should_receive(:to_str).and_return("lo") @@ -70,15 +71,32 @@ "yellow moon".encode("US-ASCII").squeeze("a").encoding.should == Encoding::US_ASCII end - it "raises a TypeError when one set arg can't be converted to a string" do + it "raises a TypeError when an argument can't be converted to a string" do -> { "hello world".squeeze([]) }.should.raise(TypeError) -> { "hello world".squeeze(Object.new)}.should.raise(TypeError) -> { "hello world".squeeze(mock('x')) }.should.raise(TypeError) end + it "respects backslash for escaping" do + "aa--bb".squeeze("a\\-b").should == "a-b" + "^^".squeeze("\\^").should == "^" + "\\\\".squeeze("\\\\").should == "\\" + end + + it "squeezes multibyte characters" do + "\u{56db}\u{56db}\u{6708}\u{6708}".squeeze("\u{6708}").should == "\u{56db}\u{56db}\u{6708}" + "\u{56db}\u{56db}\u{6708}\u{6708}".squeeze("\u{56db}").should == "\u{56db}\u{6708}\u{6708}" + "\u{56db}\u{56db}\u{6708}\u{6708}".squeeze("\u{56db}\u{6708}").should == "\u{56db}\u{6708}" + end + it "returns String instances when called on a subclass" do StringSpecs::MyString.new("oh no!!!").squeeze("!").should.instance_of?(String) end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".squeeze("e".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").squeeze("e") }.should.raise(Encoding::CompatibilityError) + end end describe "String#squeeze!" do diff --git a/spec/ruby/core/string/strip_spec.rb b/spec/ruby/core/string/strip_spec.rb index 81994a7f2e2fa5..583cdb231a6424 100644 --- a/spec/ruby/core/string/strip_spec.rb +++ b/spec/ruby/core/string/strip_spec.rb @@ -15,6 +15,81 @@ it "returns a copy of self without leading and trailing NULL bytes and whitespace" do " \x00 goodbye \x00 ".strip.should == "goodbye" end + + ruby_version_is "4.0" do + context "when given character selectors arguments" do + it "removes leading and trailing characters in the intersection of sets removed" do + " hello ".strip(" ").should == "hello" + "llo".strip("lo", "l").should == "o" + "llo".strip("o", "lo").should == "ll" + "hello".strip("ho", "h").should == "ello" + "hell yeah".strip("").should == "hell yeah" + end + + it "negates sets starting with ^" do + "ello".strip("aeiou", "^e").should == "ell" + "hello".strip("^o").should == "o" + end + + it "removes leading and trailing characters in a sequence" do + "hello".strip("l-o").should == "he" + "hello".strip("e-h").should == "llo" + "hel-lo".strip("h-").should == "el-lo" + "hel-lo".strip("-o").should == "hel-l" + "abcdefgh".strip("a-ce-fh").should == "defg" + "abcdefgh".strip("he-fa-c").should == "defg" + "abcdefgh".strip("e-fha-c").should == "defg" + "abcde".strip("ac-e").should == "b" + "abcde".strip("^ac-e").should == "abcde" + end + + it "removes leading and trailing multibyte characters" do + "四月".strip("月").should == "四" + "四月".strip("四").should == "月" + "哥哥我倒".strip("哥").should == "我倒" + end + + it "respects backslash for escaping" do + "a-b".strip("a\\-b").should == "" + "^".strip("\\^").should == "" + "\\".strip("\\\\").should == "" + end + + it "raises an ArgumentError when the sequence is invalid" do + -> { "hello".strip("h-e") }.should.raise(ArgumentError) + -> { "hello".strip("^h-e") }.should.raise(ArgumentError) + end + + it "tries to convert each argument to a string using to_str" do + other_string = mock('h') + other_string.should_receive(:to_str).and_return("h") + + other_string2 = mock('he') + other_string2.should_receive(:to_str).and_return("he") + + "hello world".strip(other_string, other_string2).should == "ello world" + end + + it "raises a TypeError when an argument can't be converted to a string" do + -> { "hello world".strip(100) }.should.raise(TypeError) + -> { "hello world".strip([]) }.should.raise(TypeError) + -> { "hello world".strip(mock('x')) }.should.raise(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("oh no!!!").strip("!").should.instance_of?(String) + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").strip("h").encoding.should == Encoding::US_ASCII + end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".strip("e".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").strip("e") }.should.raise(Encoding::CompatibilityError) + end + end + end end describe "String#strip!" do @@ -23,21 +98,45 @@ a.strip!.should.equal?(a) a.should == "hello" - a = "\tgoodbye\r\v\n" - a.strip! - a.should == "goodbye" + ruby_version_is "4.0" do + a = "---goodbye---" + a.strip!("-") + a.should == "goodbye" + end end it "returns nil if no modifications where made" do a = "hello" a.strip!.should == nil a.should == "hello" + + ruby_version_is "4.0" do + a = "hello" + a.strip!("-").should == nil + a.should == "hello" + end end it "makes a string empty if it is only whitespace" do - "".strip!.should == nil - " ".strip.should == "" - " ".strip.should == "" + a = "" + a.strip! + a.should == "" + + a = " " + a.strip! + a.should == "" + end + + ruby_version_is "4.0" do + it "makes a string empty if all characters match given character selectors" do + a = "" + a.strip!("-") + a.should == "" + + a = "----" + a.strip!("-") + a.should == "" + end end it "removes leading and trailing NULL bytes and whitespace" do @@ -48,6 +147,9 @@ it "raises a FrozenError on a frozen instance that is modified" do -> { " hello ".freeze.strip! }.should.raise(FrozenError) + ruby_version_is "4.0" do + -> { " hello ".freeze.strip!("-") }.should.raise(FrozenError) + end end # see #1552 diff --git a/spec/ruby/core/string/tr_s_spec.rb b/spec/ruby/core/string/tr_s_spec.rb index 22a193ec4be794..b3185b4cbd84ee 100644 --- a/spec/ruby/core/string/tr_s_spec.rb +++ b/spec/ruby/core/string/tr_s_spec.rb @@ -16,6 +16,9 @@ "123456789".tr_s("2-5", "abcdefg").should == "1abcd6789" "hello ^--^".tr_s("e-", "__").should == "h_llo ^_^" "hello ^--^".tr_s("---", "_").should == "hello ^_^" + "hello ^--^".tr_s("-e", "_a").should == "hallo ^_^" + "hel-001122".tr_s("--2", "a-f").should == "heladef" + "hel-(())".tr_s("(--", "a-f").should == "helfab" end it "accepts c1-c1 notation to denote range of one character" do @@ -29,6 +32,26 @@ "this".tr_s("this", "x").should == "x" end + it "raises an ArgumentError when given wrong number of arguments" do + -> { "hello".tr_s }.should.raise(ArgumentError) + -> { "hello".tr_s("a") }.should.raise(ArgumentError) + -> { "hello".tr_s("a", "b", "c") }.should.raise(ArgumentError) + end + + it "raises an ArgumentError when the replacement contains a descending range" do + -> { "hello".tr_s("a-y", "z-b") }.should.raise(ArgumentError) + end + + it "raises an ArgumentError when the source contains a descending range" do + -> { "hello".tr_s("l-a", "z") }.should.raise(ArgumentError) + end + + it "returns self (or a copy) when from_string or to_string is empty" do + "hello".tr_s("", "a").should == "hello" + "hello".tr_s("a", "").should == "hello" + "hello".tr_s("", "").should == "hello" + end + it "translates chars not in from_string when it starts with a ^" do "hello".tr_s('^aeiou', '*').should == "*e*o" "123456789".tr_s("^345", "abc").should == "c345c" @@ -43,7 +66,7 @@ "hello ^-^".tr_s("^---l-o", "x").should == "xllox-x" end - it "tries to convert from_str and to_str to strings using to_str" do + it "tries to convert each argument to a string using to_str" do from_str = mock('ab') from_str.should_receive(:to_str).and_return("ab") @@ -97,7 +120,30 @@ str.tr_s(a, b).should == "椎名深夏" end + it "respects backslash for escaping" do + "aa--bb".tr_s("a\\-b", "123").should == "123" + "^^".tr_s("\\^", "1").should == "1" + "\\\\".tr_s("\\\\", "1").should == "1" + end + + it "raises a TypeError when an argument can't be converted to a string" do + -> { "hello".tr_s(100, "a") }.should.raise(TypeError) + -> { "hello".tr_s("a", []) }.should.raise(TypeError) + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").tr_s("l", "r").encoding.should == Encoding::US_ASCII + end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".tr_s("e".encode("UTF-16LE"), "a") }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").tr_s("e", "a") }.should.raise(Encoding::CompatibilityError) + end + it "deletes the characters in from_str and does not squeeze the result if to_str is empty" do + "hello".tr_s("el", "").should == "ho" + "hellooo".tr_s("el", "").should == "hooo" + end end describe "String#tr_s!" do diff --git a/spec/ruby/core/string/tr_spec.rb b/spec/ruby/core/string/tr_spec.rb index 98551d4ab83b3e..91e0d2afa1c7d1 100644 --- a/spec/ruby/core/string/tr_spec.rb +++ b/spec/ruby/core/string/tr_spec.rb @@ -15,6 +15,9 @@ "123456789".tr("2-5","abcdefg").should == "1abcd6789" "hello ^-^".tr("e-", "__").should == "h_llo ^_^" "hello ^-^".tr("---", "_").should == "hello ^_^" + "hello ^-^".tr("-e", "_a").should == "hallo ^_^" + "hel-012".tr("--2", "a-f").should == "heladef" + "hel-()".tr("(--", "a-f").should == "helfab" end it "accepts c1-c1 notation to denote range of one character" do @@ -29,14 +32,28 @@ "hello".tr("a-z", "A-H.").should == "HE..." end - it "raises an ArgumentError a descending range in the replacement as containing just the start character" do + it "raises an ArgumentError when given wrong number of arguments" do + -> { "hello".tr }.should.raise(ArgumentError) + ruby_version_is ""..."4.1" do + -> { "hello".tr("a") }.should.raise(ArgumentError) + end + -> { "hello".tr("a", "b", "c") }.should.raise(ArgumentError) + end + + it "raises an ArgumentError when the replacement contains a descending range" do -> { "hello".tr("a-y", "z-b") }.should.raise(ArgumentError) end - it "raises an ArgumentError a descending range in the source as empty" do + it "raises an ArgumentError when the source contains a descending range" do -> { "hello".tr("l-a", "z") }.should.raise(ArgumentError) end + it "returns self (or a copy) when from_string or to_string is empty" do + "hello".tr("", "a").should == "hello" + "hello".tr("a", "").should == "hello" + "hello".tr("", "").should == "hello" + end + it "translates chars not in from_string when it starts with a ^" do "hello".tr('^aeiou', '*').should == "*e**o" "123456789".tr("^345", "abc").should == "cc345cccc" @@ -55,7 +72,7 @@ "hello".tr("helo", "1212").should == "12112" end - it "tries to convert from_str and to_str to strings using to_str" do + it "tries to convert each argument to a string using to_str" do from_str = mock('ab') from_str.should_receive(:to_str).and_return("ab") @@ -65,7 +82,7 @@ "bla".tr(from_str, to_str).should == "BlA" end - it "returns Stringinstances when called on a subclass" do + it "returns String instances when called on a subclass" do StringSpecs::MyString.new("hello").tr("e", "a").should.instance_of?(String) end @@ -93,9 +110,28 @@ str.tr(a, b).should == "椎名深夏" end - it "raises Encoding::CompatibilityError when from_string or to_string parameters have an incompatible encoding" do - -> { "fée".tr("é".encode(Encoding::ISO_8859_1), "e") }.should.raise(Encoding::CompatibilityError) - -> { "fée".tr("e", "é".encode(Encoding::ISO_8859_1)) }.should.raise(Encoding::CompatibilityError) + it "respects backslash for escaping" do + "a-b".tr("a\\-b", "123").should == "123" + "^".tr("\\^", "1").should == "1" + "\\".tr("\\\\", "1").should == "1" + end + + it "raises a TypeError when an argument can't be converted to a string" do + -> { "hello".tr(100, "a") }.should.raise(TypeError) + -> { "hello".tr("a", []) }.should.raise(TypeError) + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").tr("l", "r").encoding.should == Encoding::US_ASCII + end + + it "raises an Encoding::CompatibilityError when the encodings are incompatible" do + -> { "hello".tr("e".encode("UTF-16LE"), "a") }.should.raise(Encoding::CompatibilityError) + -> { "hello".encode("UTF-16LE").tr("e", "a") }.should.raise(Encoding::CompatibilityError) + end + + it "deletes the characters in from_str if to_str is empty" do + "hello".tr("el", "").should == "ho" end ruby_version_is "4.1" do diff --git a/spec/ruby/core/thread/backtrace/location/source_range_spec.rb b/spec/ruby/core/thread/backtrace/location/source_range_spec.rb index 1fb4c9f10aa0be..b77e1d5503e729 100644 --- a/spec/ruby/core/thread/backtrace/location/source_range_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/source_range_spec.rb @@ -3,8 +3,12 @@ ruby_version_is "4.1" do describe "Thread::Backtrace::Location#source_range" do + before do + skip "parse.y" if proc {}.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + end + it "returns a Ruby::SourceRange with the location paths" do - location, range, path, absolute_path = capture_backtrace_location_source_range(<<-RUBY) + location, range, path, absolute_path = capture_backtrace_location_source_range(<<-RUBY, :CallNode) $nil.foo$ RUBY @@ -16,111 +20,111 @@ end { - "receiver calls with arguments" => <<-RUBY, + "receiver calls with arguments" => [<<-RUBY, :CallNode], $nil.foo(42)$ RUBY - "receiver calls split across lines" => <<-RUBY, + "receiver calls split across lines" => [<<-RUBY, :CallNode], $nil .foo( 42 )$ RUBY - "safe navigation calls" => <<-RUBY, + "safe navigation calls" => [<<-RUBY, :CallNode], $1&.foo(42)$ RUBY - ".() call syntax" => <<-RUBY, + ".() call syntax" => [<<-RUBY, :CallNode], $nil.(42)$ RUBY - "calls to send" => <<-RUBY, + "calls to send" => [<<-RUBY, :CallNode], $nil.send(:foo, 42)$ RUBY - "index reads" => <<-RUBY, + "index reads" => [<<-RUBY, :CallNode], $nil[0]$ RUBY - "index writes" => <<-RUBY, + "index writes" => [<<-RUBY, :CallNode], $nil[0] = 42$ RUBY - "explicit index write calls" => <<-RUBY, + "explicit index write calls" => [<<-RUBY, :CallNode], $nil.[]=$ RUBY - "attribute writes" => <<-RUBY, + "attribute writes" => [<<-RUBY, :CallNode], $nil.foo = 42$ RUBY - "binary operator calls split by a comment" => <<-RUBY, + "binary operator calls split by a comment" => [<<-RUBY, :CallNode], $nil + # comment 42$ RUBY - "unary operator calls" => <<-RUBY, + "unary operator calls" => [<<-RUBY, :CallNode], $+nil$ RUBY - "function calls" => <<-RUBY, + "function calls" => [<<-RUBY, :CallNode], "str".instance_eval { $gsub("foo", :sym)$ } RUBY - "function calls without ()" => <<-RUBY, + "function calls without ()" => [<<-RUBY, :CallNode], "str".instance_eval { $gsub "foo", :sym$ } RUBY - "variable calls" => <<-RUBY, + "variable calls" => [<<-RUBY, :CallNode], nil.instance_eval { $foo$ } RUBY - "local variable operator assignments" => <<-RUBY, + "local variable operator assignments" => [<<-RUBY, :LocalVariableOperatorWriteNode], value = nil $value += 42$ RUBY - "index operator assignments failing while reading" => <<-RUBY, + "index operator assignments failing while reading" => [<<-RUBY, :IndexOperatorWriteNode], value = nil $value[0] += 42$ RUBY - "index operator assignments failing in the operator" => <<-RUBY, + "index operator assignments failing in the operator" => [<<-RUBY, :IndexOperatorWriteNode], value = Object.new def value.[](index) = nil $value[0] += 42$ RUBY - "index operator assignments failing while writing" => <<-RUBY, + "index operator assignments failing while writing" => [<<-RUBY, :IndexOperatorWriteNode], value = Object.new def value.[](index) = 1 $value[0] += 42$ RUBY - "index operator assignments failing on an argument" => <<-RUBY, + "index operator assignments failing on an argument" => [<<-RUBY, :IndexOperatorWriteNode], value = [] $value[nil] += 42$ RUBY - "attribute operator assignments failing while reading" => <<-RUBY, + "attribute operator assignments failing while reading" => [<<-RUBY, :CallOperatorWriteNode], value = nil $value.foo += 42$ RUBY - "attribute operator assignments failing in the operator" => <<-RUBY, + "attribute operator assignments failing in the operator" => [<<-RUBY, :CallOperatorWriteNode], value = Object.new def value.foo = nil $value.foo += 42$ RUBY - "attribute operator assignments failing while writing" => <<-RUBY, + "attribute operator assignments failing while writing" => [<<-RUBY, :CallOperatorWriteNode], value = Object.new def value.foo = 1 $value.foo += 42$ RUBY - "attribute operator assignments failing on the value" => <<-RUBY, + "attribute operator assignments failing on the value" => [<<-RUBY, :CallOperatorWriteNode], value = Object.new def value.foo = 1 def value.foo=(new_value) @@ -129,84 +133,260 @@ def value.foo=(new_value) $value.foo += nil$ RUBY - "bare constants" => <<-RUBY, + "bare constants" => [<<-RUBY, :ConstantReadNode], $SourceRangeNotDefined$ RUBY - "qualified constants" => <<-RUBY, + "qualified constants" => [<<-RUBY, :ConstantPathNode], $Object::SourceRangeNotDefined$ RUBY - "qualified constants split across lines" => <<-RUBY, + "qualified constants split across lines" => [<<-RUBY, :ConstantPathNode], $Object:: SourceRangeNotDefined$ RUBY - "top-level constants" => <<-RUBY, + "top-level constants" => [<<-RUBY, :ConstantPathNode], $::SourceRangeNotDefined$ RUBY - "constant operator assignments" => <<-RUBY, + "constant operator assignments" => [<<-RUBY, :ConstantPathOperatorWriteNode], namespace = Module.new namespace.const_set(:Nil, nil) $namespace::Nil += 1$ RUBY - "constant operator assignments failing while reading" => <<-RUBY, + # This covers the whole expression for consistency with other operator assignments + # where there is no "read node": https://bugs.ruby-lang.org/issues/22235 + "constant operator assignments failing while reading" => [<<-RUBY, :ConstantPathOperatorWriteNode], namespace = Module.new $namespace::NotDefined += 1$ RUBY - "top-level constant operator assignments" => <<-RUBY, + "top-level ::constant operator assignments" => [<<-RUBY, :ConstantPathOperatorWriteNode], $::SourceRangeNotDefined += 1$ RUBY - "explicit raises" => <<-RUBY, + "top-level constant operator assignments" => [<<-RUBY, :ConstantOperatorWriteNode], + $SourceRangeNotDefined += 1$ + RUBY + + "explicit #raise" => [<<-RUBY, :CallNode], $raise NameError$ RUBY - "calls failing while converting arguments" => <<-RUBY, + "calls failing while converting arguments" => [<<-RUBY, :CallNode], $1.+(nil)$ RUBY - "calls with brace blocks" => <<-RUBY, + "calls with brace blocks" => [<<-RUBY, :CallNode], $nil.foo(1) { 2 }$ RUBY - "calls with do-end blocks" => <<-RUBY, + "calls with do-end blocks" => [<<-RUBY, :CallNode], $nil.foo(1) do 2 end$ RUBY - "calls with heredoc arguments" => <<-RUBY, + "calls with heredoc arguments" => [<<-RUBY, :CallNode], $nil.foo(<<~TEXT)$ heredoc TEXT RUBY - "source with a data section" => "$nil.foo$\n__END__\ndata\n", + "source with a data section" => ["$nil.foo$\n__END__\ndata\n", :CallNode], - "__END__ inside a heredoc" => "value = < ["value = < <<-RUBY, + "multibyte identifiers with byte columns" => [<<-RUBY, :CallNode], value = "été" $value.あいうえお$ RUBY - "hard tabs" => "\t \t$1.time {}$\n", + "hard tabs" => ["\t \t$1.time {}$\n", :CallNode], + + "a missing final newline" => ["$1.time {}$", :CallNode], + + "very long source lines" => [("1" * 100) + " + $1.time {}$\n", :CallNode], + + "-> { it } called with no arguments" => [<<-RUBY, :LambdaNode], + $-> { it }$.call() + RUBY + + "lambda with rescue" => [<<-RUBY, :BlockNode], + lambda $do; rescue; end$.call(1) + RUBY + + "def with rescue" => [<<-RUBY, :DefNode], + value = Object.new + $def value.f(a); rescue => e; e; end$ + value.f() + RUBY + + "block with it parameters" => [<<-RUBY, :BlockNode], + value = lambda $do + it + end$ + value.call + RUBY + + "block with numbered parameters" => [<<-RUBY, :BlockNode], + value = lambda ${ _1 }$ + value.call + RUBY + + "lambda with numbered parameters" => [<<-RUBY, :LambdaNode], + value = $-> { _1 }$ + value.call + RUBY - "a missing final newline" => "$1.time {}$", + "hash pattern with implicit key" => [<<-RUBY, :HashPatternNode], + value = Object.new + def value.deconstruct_keys(keys) = nil + case value + in $key:$ + end + RUBY + + "hash pattern with value" => [<<-RUBY, :HashPatternNode], + value = Object.new + def value.deconstruct_keys(keys) = nil + case value + in $key: 1$ + end + RUBY + + "hash pattern with rest" => [<<-RUBY, :HashPatternNode], + value = Object.new + def value.deconstruct_keys(keys) = nil + case value + in $**rest$ + end + RUBY + + "hash pattern rejecting extra keys" => [<<-RUBY, :HashPatternNode], + value = Object.new + def value.deconstruct_keys(keys) = nil + case value + in $**nil$ + end + RUBY + + "array pattern with splat" => [<<-RUBY, :ArrayPatternNode], + value = Object.new + def value.deconstruct = nil + case value + in $*rest$ + end + RUBY + + "keyword hash with splat" => [<<-RUBY, :KeywordHashNode], + value = Object.new + def value.to_hash = 1 + [$**value$] + RUBY + + "keyword hash with association" => [<<-RUBY, :KeywordHashNode], + key = Object.new + def key.hash = nil + [$key => 1$] + RUBY - "very long source lines" => ("1" * 100) + " + $1.time {}$\n", - }.each do |description, source| + "array with splat" => [<<-RUBY, :ArrayNode], + value = Object.new + def value.to_a = 1 + result = $*value$ + RUBY + + "singleton class with rescue" => [<<-RUBY, :SingletonClassNode], + value = 1 + $class << value; rescue; end$ + RUBY + + "class with rescue" => [<<-RUBY, :ClassNode], + SourceRangeClassSpecs = 1 + $class SourceRangeClassSpecs; rescue; end$ + RUBY + + "module with rescue" => [<<-RUBY, :ModuleNode], + SourceRangeModuleSpecs = 1 + $module SourceRangeModuleSpecs; rescue; end$ + RUBY + + # These nodes can own real calls, so source-range lookup cannot always exclude their classes. + "symbol used for case equality" => [<<-RUBY, :SymbolNode, 1], + class Symbol + alias_method :source_range_original_case_equal, :=== + def ===(other) = raise(TypeError) + end + begin + case :other + when $:expected$ + end + ensure + class Symbol + alias_method :===, :source_range_original_case_equal + remove_method :source_range_original_case_equal + end + end + RUBY + + "splat converted for a when clause" => [<<-RUBY, :SplatNode, 1], + value = Object.new + def value.to_a = raise(TypeError) + case nil + when $*value$ + end + RUBY + + "keyword argument omission call" => [<<-RUBY, :CallNode], + def target(**keywords) = keywords + target($missing:$) + RUBY + + "splat returned from a method" => [<<-RUBY, :SplatNode, 1], + value = Object.new + def value.to_a = raise(TypeError) + def target(value) + return $*value$ + end + target(value) + RUBY + + "symbol used for pattern matching" => [<<-RUBY, :SymbolNode, 1], + class Symbol + alias_method :source_range_original_case_equal, :=== + def ===(other) = raise(TypeError) + end + begin + case :other + in $:expected$ + end + ensure + class Symbol + alias_method :===, :source_range_original_case_equal + remove_method :source_range_original_case_equal + end + end + RUBY + + # Aborts the test process on CRuby's CI with ZJIT + # "interpolated symbol" => [<<-RUBY, :InterpolatedSymbolNode], + # value = Object.new + # def value.to_s + # (+"\\xFF").force_encoding(Encoding::UTF_8) + # end + # %I[$\#{value}$] + # RUBY + }.each_pair do |description, (source, prism_class, frame)| it "returns the precise range for #{description}" do - capture_backtrace_location_source_range(source) + capture_backtrace_location_source_range(source, prism_class, frame: frame || 0) end end it "returns the method definition for a method arity error" do - capture_backtrace_location_source_range(<<-RUBY) + capture_backtrace_location_source_range(<<-RUBY, :DefNode) target = Class.new do $def source_range_target(first, second) first + second @@ -217,7 +397,7 @@ def value.foo=(new_value) end it "returns the call for the caller frame of a method arity error" do - capture_backtrace_location_source_range(<<-RUBY, frame: 1) + capture_backtrace_location_source_range(<<-RUBY, :CallNode, frame: 1) target = Class.new do def source_range_target(first, second) first + second @@ -228,7 +408,7 @@ def source_range_target(first, second) end it "returns a multiline method definition for a method arity error" do - capture_backtrace_location_source_range(<<-RUBY) + capture_backtrace_location_source_range(<<-RUBY, :DefNode) target = Class.new do $def source_range_target( first, @@ -242,7 +422,7 @@ def source_range_target(first, second) end it "returns a singleton method definition with spacing for a keyword arity error" do - capture_backtrace_location_source_range(<<-RUBY) + capture_backtrace_location_source_range(<<-RUBY, :DefNode) target = Object.new $def target . source_range_target(value:) value @@ -252,21 +432,21 @@ def source_range_target(first, second) end it "returns a stabby lambda for an arity error" do - capture_backtrace_location_source_range(<<-RUBY) + capture_backtrace_location_source_range(<<-RUBY, :LambdaNode) value = $->(argument) {}$ value.call RUBY end it "returns only the block for an arity error in a Kernel#lambda" do - capture_backtrace_location_source_range(<<-RUBY) + capture_backtrace_location_source_range(<<-RUBY, :BlockNode) value = lambda ${ |argument| }$ value.call RUBY end it "returns only the block for an arity error in a define_method" do - capture_backtrace_location_source_range(<<-RUBY) + capture_backtrace_location_source_range(<<-RUBY, :BlockNode) target = Class.new do define_method(:source_range_target) $do |first, second| first + second @@ -276,28 +456,6 @@ def source_range_target(first, second) RUBY end - it "raises for a location without Ruby bytecode" do - report_on_exception = Thread.report_on_exception - Thread.report_on_exception = false - - begin - thread = Thread.new(&method(:throw)) - exception = begin - thread.value - rescue ArgumentError => error - error - end - location = exception.backtrace_locations.first - - location.path.should == nil - -> { - location.source_range - }.should.raise(RuntimeError, "cannot get source range for location without Ruby bytecode") - ensure - Thread.report_on_exception = report_on_exception - end - end - it "propagates an error when the absolute source file no longer exists" do keep_source(false) do location, path = capture_backtrace_location_from_source("nil.foo\n") @@ -412,11 +570,13 @@ def source_range_target(first, second) end it "does not treat a method from eval named -e as command-line source" do - code = "eval(%q{def spoofed_source_range_target; nil.foo; end}, binding, %q{-e}); " \ - "begin; spoofed_source_range_target; rescue => e; " \ - "begin; e.backtrace_locations.first.source_range; rescue => source_error; " \ - "p source_error; end; end" - ruby_exe(code, escape: false).should == "#\n" + keep_source(false) do # skip if always keep source + code = "eval(%q{def spoofed_source_range_target; nil.foo; end}, binding, %q{-e}); " \ + "begin; spoofed_source_range_target; rescue => e; " \ + "begin; e.backtrace_locations.first.source_range; rescue => source_error; " \ + "p source_error; end; end" + ruby_exe(code, escape: false).should == "#\n" + end end it "works for -e source" do diff --git a/spec/ruby/core/thread/backtrace/location/syntax_tree_spec.rb b/spec/ruby/core/thread/backtrace/location/syntax_tree_spec.rb new file mode 100644 index 00000000000000..0c7580edaec976 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/syntax_tree_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../../spec_helper' + +ruby_version_is "4.1" do + describe "Thread::Backtrace::Location#syntax_tree" do + # This is tested more extensively in core/thread/backtrace/location/source_range_spec.rb + + before do + skip "parse.y" if proc {}.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + end + + it "returns a CallNode for the first location from caller_locations" do + node = -> { caller_locations(0, 1)[0] }.call.syntax_tree + node.start_line.should == __LINE__ - 1 + node.should.is_a?(Prism::CallNode) + node.name.should == :caller_locations + end + end +end diff --git a/spec/ruby/core/unboundmethod/syntax_tree_spec.rb b/spec/ruby/core/unboundmethod/syntax_tree_spec.rb new file mode 100644 index 00000000000000..f1239effd880e6 --- /dev/null +++ b/spec/ruby/core/unboundmethod/syntax_tree_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative '../method/shared/syntax_tree' + +ruby_version_is "4.1" do + describe "UnboundMethod#syntax_tree" do + before :each do + @object = -> method { method.unbind } + + skip "parse.y" if method(:it).unbind.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + end + + it_behaves_like :method_syntax_tree, :syntax_tree + end +end diff --git a/spec/ruby/default.mspec b/spec/ruby/default.mspec index c8b1215f561d9c..5516915c735254 100644 --- a/spec/ruby/default.mspec +++ b/spec/ruby/default.mspec @@ -50,5 +50,6 @@ class MSpecScript /\wSpecs?$/, /^CS_CONST/, /^CSL_CONST/, + /^Prism$/, ] end diff --git a/spec/ruby/fixtures/source_range_helpers.rb b/spec/ruby/fixtures/source_range_helpers.rb index aeed5cc8944f34..5e73a391f8f983 100644 --- a/spec/ruby/fixtures/source_range_helpers.rb +++ b/spec/ruby/fixtures/source_range_helpers.rb @@ -3,6 +3,15 @@ def source_range_values(range) end def keep_source(value = true) + always_keeps_source = RUBY_ENGINE == "truffleruby" + if always_keeps_source + if value + yield + else + skip "This Ruby implementation always keeps the source in memory so cannot run specs which rely on not keeping it" + end + end + return yield unless defined?(RubyVM.keep_script_lines) previous = RubyVM.keep_script_lines @@ -44,7 +53,7 @@ def check_source_range(marked_source) result.source_location[1].should == expected[0] end -def capture_backtrace_location_source_range(marked_source, frame: 0) +def capture_backtrace_location_source_range(marked_source, prism_class, frame: 0) source, expected = source_range_source(marked_source) path = tmp("backtrace_location_source_range.rb") File.binwrite(path, source) @@ -59,11 +68,33 @@ def capture_backtrace_location_source_range(marked_source, frame: 0) raise "Expected source to raise an exception" unless exception - location = exception.backtrace_locations.fetch(frame) + total_frames = 0 + user_frames = 0 + while true + location = exception.backtrace_locations.fetch(total_frames) + if location.path.start_with?(" { + :"#{(+"\xFF").force_encoding(Encoding::UTF_8)}" + }.should.raise(EncodingError, 'invalid symbol in encoding UTF-8 :"\xFF"') + + -> { + %I[#{(+"\xFF").force_encoding(Encoding::UTF_8)}] + }.should.raise(EncodingError, 'invalid symbol in encoding UTF-8 :"\xFF"') + end + end + ruby_bug "#20280", ""..."3.4" do it "raises an SyntaxError at parse time when Symbol with invalid bytes" do ScratchPad.record [] diff --git a/spec/ruby/optional/capi/array_spec.rb b/spec/ruby/optional/capi/array_spec.rb index ad0eb56741efdb..72cf72477ade1d 100644 --- a/spec/ruby/optional/capi/array_spec.rb +++ b/spec/ruby/optional/capi/array_spec.rb @@ -273,6 +273,17 @@ b.should == [1, 2, 3] a.should == [1, 2, 3] # check a was not modified end + + it "keeps Float elements alive across GC and later accesses" do + a = [1.5, 2.5, 3.5] + @s.RARRAY_PTR_iterate(a) { |e| } + GC.start + b = [] + @s.RARRAY_PTR_iterate(a) do |e| + b << e + end + b.should == [1.5, 2.5, 3.5] + end end describe "RARRAY_LEN" do diff --git a/spec/ruby/optional/capi/encoding_spec.rb b/spec/ruby/optional/capi/encoding_spec.rb index b77a967b1e11e9..efb1b049833e77 100644 --- a/spec/ruby/optional/capi/encoding_spec.rb +++ b/spec/ruby/optional/capi/encoding_spec.rb @@ -106,6 +106,16 @@ end end + describe "rb_enc_dummy_p" do + it "returns true for a dummy encoding" do + @s.rb_enc_dummy_p(Encoding::UTF_16).should == true + end + + it "returns false for a non-dummy encoding" do + @s.rb_enc_dummy_p(Encoding::UTF_8).should == false + 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 2038a5d4a85e50..4e67f7af7b9e6d 100644 --- a/spec/ruby/optional/capi/ext/encoding_spec.c +++ b/spec/ruby/optional/capi/ext/encoding_spec.c @@ -101,6 +101,10 @@ static VALUE encoding_spec_rb_enc_copy(VALUE self, VALUE dest, VALUE src) { return dest; } +static VALUE encoding_spec_rb_enc_dummy_p(VALUE self, VALUE encoding) { + return rb_enc_dummy_p(rb_to_encoding(encoding)) ? Qtrue : Qfalse; +} + static VALUE encoding_spec_rb_enc_find(VALUE self, VALUE name) { return rb_str_new2(rb_enc_find(RSTRING_PTR(name))->name); } @@ -362,6 +366,7 @@ void Init_encoding_spec(void) { 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_dummy_p", encoding_spec_rb_enc_dummy_p, 1); 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/io_spec.c b/spec/ruby/optional/capi/ext/io_spec.c index 7aaceef0c06797..be9f830db66431 100644 --- a/spec/ruby/optional/capi/ext/io_spec.c +++ b/spec/ruby/optional/capi/ext/io_spec.c @@ -419,6 +419,9 @@ void Init_io_spec(void) { rb_define_const(cls, "FMODE_SYNC", INT2FIX(FMODE_SYNC)); rb_define_const(cls, "FMODE_BINMODE", INT2FIX(FMODE_BINMODE)); rb_define_const(cls, "FMODE_TEXTMODE", INT2FIX(FMODE_TEXTMODE)); + rb_define_const(cls, "FMODE_CREATE", INT2FIX(FMODE_CREATE)); + rb_define_const(cls, "FMODE_APPEND", INT2FIX(FMODE_APPEND)); + rb_define_const(cls, "FMODE_TRUNC", INT2FIX(FMODE_TRUNC)); rb_define_const(cls, "ECONV_UNIVERSAL_NEWLINE_DECORATOR", INT2FIX(ECONV_UNIVERSAL_NEWLINE_DECORATOR)); } diff --git a/spec/ruby/optional/capi/io_spec.rb b/spec/ruby/optional/capi/io_spec.rb index 6db95611cd3a9c..a2802994c026c0 100644 --- a/spec/ruby/optional/capi/io_spec.rb +++ b/spec/ruby/optional/capi/io_spec.rb @@ -669,6 +669,44 @@ (@o.rb_io_mode(@w_io) & 0b11).should == 0b10 (@o.rb_io_mode(@rw_io) & 0b11).should == 0b11 end + + it "includes FMODE_APPEND when in append mode" do + io = File.open(@name, "a") + begin + (@o.rb_io_mode(io) & CApiIOSpecs::FMODE_APPEND).should == CApiIOSpecs::FMODE_APPEND + ensure + io.close + end + end + + it "includes FMODE_BINMODE when in binary mode" do + io = File.open(@name, "rb") + begin + (@o.rb_io_mode(io) & CApiIOSpecs::FMODE_BINMODE).should == CApiIOSpecs::FMODE_BINMODE + ensure + io.close + end + end + + it "includes FMODE_CREATE when the file is created" do + name = tmp("rb_io_mode_specs") + io = File.open(name, "w") + begin + (@o.rb_io_mode(io) & CApiIOSpecs::FMODE_CREATE).should == CApiIOSpecs::FMODE_CREATE + ensure + io.close + rm_r name + end + end + + it "includes FMODE_TRUNC when the file is truncated" do + io = File.open(@name, "w") + begin + (@o.rb_io_mode(io) & CApiIOSpecs::FMODE_TRUNC).should == CApiIOSpecs::FMODE_TRUNC + ensure + io.close + end + end end describe "rb_io_path" do diff --git a/string.c b/string.c index 4327492619f7d7..7226e7287f700f 100644 --- a/string.c +++ b/string.c @@ -9327,17 +9327,14 @@ tr_trans_pairs_search_basic(struct tr_trans_pairs_search *search) } #ifdef HAVE_SIMD_SSE2 -static inline bool +static inline VALUE tr_trans_pairs_next_match_sse2(struct tr_trans_pairs_search *search) { size_t next_match_offset = ntz_int32(search->matches_bitmap); search->matches_bitmap >>= (next_match_offset + 1); search->s += next_match_offset; - if (search->s > search->send) { - search->s = search->send; - return false; - } - return true; + RUBY_ASSERT(search->s <= search->send); + return search->trans_table[*search->s]; } static inline VALUE @@ -9385,17 +9382,14 @@ tr_trans_pairs_search_sse2(struct tr_trans_pairs_search *search) #endif #ifdef HAVE_SIMD_NEON -static inline bool +static inline VALUE tr_trans_pairs_next_match_neon(struct tr_trans_pairs_search *search) { size_t next_match_offset = ntz_int64(search->matches_bitmap) / 4; search->matches_bitmap >>= (next_match_offset + 1) * 4; search->s += next_match_offset; - if (search->s > search->send) { - search->s = search->send; - return false; - } - return true; + RUBY_ASSERT(search->s <= search->send); + return search->trans_table[*search->s]; } static inline VALUE @@ -9529,6 +9523,7 @@ tr_trans_pairs(VALUE str, VALUE pairs_val) continue; } } + RUBY_ASSERT(RB_TYPE_P(repl, T_STRING)); modify = true; diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index ed797db965619f..0accd8af6d81d2 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -24,7 +24,6 @@ class NewlineTest < TestCase ruby/parser_test.rb ruby/ripper_test.rb ruby/ruby_parser_test.rb - ruby/parameters_signature_test.rb ] base = __dir__ diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 74398486713f67..5b21ee361dfad2 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -958,4 +958,30 @@ def test_move_object_with_finalizer $stdout.puts "sent" RUBY end + + def test_attached_object_of_unshareable_object + omit 'objspace per Ractor is how an object\'s owner is known' unless GC.config[:implementation] == 'default' + assert_ractor(<<~'RUBY') + # A singleton class is shareable whatever it is attached to, so sending one used to + # hand the attached object to another Ractor through #attached_object. + o = Object.new + assert_equal true, Ractor.shareable?(o.singleton_class) + assert_same o, o.singleton_class.attached_object + + r = Ractor.new(o.singleton_class) do |sc| + begin + sc.attached_object + rescue Ractor::IsolationError + :isolated + end + end + assert_equal :isolated, r.value + + # A shareable attached object, and a Ractor's own unshareable one, are fine. + shareable = Ractor.make_shareable(Object.new) + assert_same shareable, Ractor.new(shareable.singleton_class) { |sc| sc.attached_object }.value + assert_same String, Ractor.new(String.singleton_class) { |sc| sc.attached_object }.value + assert_equal true, Ractor.new { own = Object.new; own.singleton_class.attached_object.equal?(own) }.value + RUBY + end end diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 18629f50d463cd..5f1edff5b9d0ab 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -2688,6 +2688,11 @@ def test_tr assert_equal(S("XYC"), S("ABC").tr("A-AB", "XY")) end + def test_tr_hash + assert_equal S(("A"*16 + "x" + "B"*3)), S(("A"*16 + "<" + "B"*3)).tr({"<"=>"x"}) + assert_equal S(("x"+"A"*16 + "B"*3)), S(("<"+"A"*16 + "B"*3)).tr({"<"=>"x"}) + end + def test_tr! a = S("hello") b = a.dup diff --git a/win32/win32.c b/win32/win32.c index d6d4acbb817ad2..59e875ac903747 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -1021,6 +1021,7 @@ static const char szInternalCmds[][InternalCmdsMax+2] = { "\1" "lock", "\3" "md", "\3" "mkdir", + "\2" "mklink", "\2" "move", "\3" "path", "\3" "pause",