diff options
Diffstat (limited to 'spec/ruby/core/io')
123 files changed, 10689 insertions, 0 deletions
diff --git a/spec/ruby/core/io/advise_spec.rb b/spec/ruby/core/io/advise_spec.rb new file mode 100644 index 0000000000..651fc52378 --- /dev/null +++ b/spec/ruby/core/io/advise_spec.rb @@ -0,0 +1,86 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#advise" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "raises a TypeError if advise is not a Symbol" do + -> { + @io.advise("normal") + }.should raise_error(TypeError) + end + + it "raises a TypeError if offset cannot be coerced to an Integer" do + -> { + @io.advise(:normal, "wat") + }.should raise_error(TypeError) + end + + it "raises a TypeError if len cannot be coerced to an Integer" do + -> { + @io.advise(:normal, 0, "wat") + }.should raise_error(TypeError) + end + + it "raises a RangeError if offset is too big" do + -> { + @io.advise(:normal, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a RangeError if len is too big" do + -> { + @io.advise(:normal, 0, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a NotImplementedError if advise is not recognized" do + ->{ + @io.advise(:foo) + }.should raise_error(NotImplementedError) + end + + it "supports the normal advice type" do + @io.advise(:normal).should be_nil + end + + it "supports the sequential advice type" do + @io.advise(:sequential).should be_nil + end + + it "supports the random advice type" do + @io.advise(:random).should be_nil + end + + it "supports the dontneed advice type" do + @io.advise(:dontneed).should be_nil + end + + it "supports the noreuse advice type" do + @io.advise(:noreuse).should be_nil + end + + platform_is_not :linux do + it "supports the willneed advice type" do + @io.advise(:willneed).should be_nil + end + end + + guard -> { platform_is :linux and kernel_version_is '3.6' } do # [ruby-core:65355] tmpfs is not supported + it "supports the willneed advice type" do + @io.advise(:willneed).should be_nil + end + end + + it "raises an IOError if the stream is closed" do + @io.close + -> { @io.advise(:normal) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/autoclose_spec.rb b/spec/ruby/core/io/autoclose_spec.rb new file mode 100644 index 0000000000..715ada7c93 --- /dev/null +++ b/spec/ruby/core/io/autoclose_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#autoclose?" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.autoclose = true unless @io.closed? + @io.close unless @io.closed? + end + + it "is set to true by default" do + @io.should.autoclose? + end + + it "cannot be queried on a closed IO object" do + @io.close + -> { @io.autoclose? }.should raise_error(IOError, /closed stream/) + end +end + +describe "IO#autoclose=" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.autoclose = true unless @io.closed? + @io.close unless @io.closed? + end + + it "can be set to true" do + @io.autoclose = false + @io.autoclose = true + @io.should.autoclose? + end + + it "can be set to false" do + @io.autoclose = true + @io.autoclose = false + @io.should_not.autoclose? + end + + it "can be set to any truthy value" do + @io.autoclose = false + @io.autoclose = 42 + @io.should.autoclose? + + @io.autoclose = false + @io.autoclose = Object.new + @io.should.autoclose? + end + + it "can be set to any falsy value" do + @io.autoclose = true + @io.autoclose = nil + @io.should_not.autoclose? + end + + it "can be set multiple times" do + @io.autoclose = true + @io.should.autoclose? + + @io.autoclose = false + @io.should_not.autoclose? + + @io.autoclose = true + @io.should.autoclose? + end + + it "cannot be set on a closed IO object" do + @io.close + -> { @io.autoclose = false }.should raise_error(IOError, /closed stream/) + end +end diff --git a/spec/ruby/core/io/binmode_spec.rb b/spec/ruby/core/io/binmode_spec.rb new file mode 100644 index 0000000000..342cac2a9b --- /dev/null +++ b/spec/ruby/core/io/binmode_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#binmode" do + before :each do + @name = tmp("io_binmode.txt") + end + + after :each do + @io.close if @io and !@io.closed? + rm_r @name + end + + it "returns self" do + @io = new_io(@name) + @io.binmode.should equal(@io) + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.binmode }.should raise_error(IOError) + end + + it "sets external encoding to binary" do + @io = new_io(@name, "w:utf-8") + @io.binmode + @io.external_encoding.should == Encoding::BINARY + end + + it "sets internal encoding to nil" do + @io = new_io(@name, "w:utf-8:ISO-8859-1") + @io.binmode + @io.internal_encoding.should == nil + end +end + +describe "IO#binmode?" do + before :each do + @filename = tmp("IO_binmode_file") + @file = File.open(@filename, "w") + @duped = nil + end + + after :each do + @duped.close if @duped + @file.close + rm_r @filename + end + + it "is true after a call to IO#binmode" do + @file.binmode?.should be_false + @file.binmode + @file.binmode?.should be_true + end + + it "propagates to dup'ed IO objects" do + @file.binmode + @duped = @file.dup + @duped.binmode?.should == @file.binmode? + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.binmode? }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/binread_spec.rb b/spec/ruby/core/io/binread_spec.rb new file mode 100644 index 0000000000..9e36b84da9 --- /dev/null +++ b/spec/ruby/core/io/binread_spec.rb @@ -0,0 +1,57 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.binread" do + before :each do + @internal = Encoding.default_internal + + @fname = tmp('io_read.txt') + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + end + + after :each do + rm_r @fname + Encoding.default_internal = @internal + end + + it "reads the contents of a file" do + IO.binread(@fname).should == @contents + end + + it "reads the contents of a file up to a certain size when specified" do + IO.binread(@fname, 5).should == @contents.slice(0..4) + end + + it "reads the contents of a file from an offset of a specific size when specified" do + IO.binread(@fname, 5, 3).should == @contents.slice(3, 5) + end + + it "returns a String in BINARY encoding" do + IO.binread(@fname).encoding.should == Encoding::BINARY + end + + it "returns a String in BINARY encoding regardless of Encoding.default_internal" do + Encoding.default_internal = Encoding::EUC_JP + IO.binread(@fname).encoding.should == Encoding::BINARY + end + + it "raises an ArgumentError when not passed a valid length" do + -> { IO.binread @fname, -1 }.should raise_error(ArgumentError) + end + + it "raises an Errno::EINVAL when not passed a valid offset" do + -> { IO.binread @fname, 0, -1 }.should raise_error(Errno::EINVAL) + end + + ruby_version_is "3.3"..."4.0" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + cmd = "|echo ok" + -> { + IO.binread(cmd) + }.should complain(/IO process creation with a leading '\|'/) + end + end +end diff --git a/spec/ruby/core/io/binwrite_spec.rb b/spec/ruby/core/io/binwrite_spec.rb new file mode 100644 index 0000000000..8ebc86a52e --- /dev/null +++ b/spec/ruby/core/io/binwrite_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/binwrite' + +describe "IO.binwrite" do + it_behaves_like :io_binwrite, :binwrite +end diff --git a/spec/ruby/core/io/buffer/empty_spec.rb b/spec/ruby/core/io/buffer/empty_spec.rb new file mode 100644 index 0000000000..e1fd4ab6a2 --- /dev/null +++ b/spec/ruby/core/io/buffer/empty_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../../spec_helper' +require_relative 'shared/null_and_empty' + +describe "IO::Buffer#empty?" do + after :each do + @buffer&.free + @buffer = nil + end + + it_behaves_like :io_buffer_null_and_empty, :empty? + + it "is true for a 0-length String-backed buffer created with .for" do + @buffer = IO::Buffer.for("") + @buffer.empty?.should be_true + end + + ruby_version_is "3.3" do + it "is true for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.empty?.should be_true + end + end + end + + it "is true for a 0-length slice of a buffer with size > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(3, 0).empty?.should be_true + end +end diff --git a/spec/ruby/core/io/buffer/external_spec.rb b/spec/ruby/core/io/buffer/external_spec.rb new file mode 100644 index 0000000000..4377a38357 --- /dev/null +++ b/spec/ruby/core/io/buffer/external_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#external?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.external?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.external?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is true for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.external?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.external?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is true for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.external?.should be_true + end + + it "is true for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.external?.should be_true + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is true" do + IO::Buffer.string(4) do |buffer| + buffer.external?.should be_true + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.external?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.external?.should be_false + end + end + + context "created with .map" do + it "is false" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.external?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.external?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.external?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.external?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/free_spec.rb b/spec/ruby/core/io/buffer/free_spec.rb new file mode 100644 index 0000000000..f3a4918978 --- /dev/null +++ b/spec/ruby/core/io/buffer/free_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#free" do + context "with a buffer created with .new" do + it "frees internal memory and nullifies the buffer" do + buffer = IO::Buffer.new(4) + buffer.free + buffer.null?.should be_true + end + + it "frees mapped memory and nullifies the buffer" do + buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + buffer.free + buffer.null?.should be_true + end + end + + context "with a file-backed buffer created with .map" do + it "frees mapped memory and nullifies the buffer" do + File.open(__FILE__, "r") do |file| + buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + buffer.free + buffer.null?.should be_true + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = +"test" + buffer = IO::Buffer.for(string) + # Read-only buffer, can't modify the string. + buffer.free + buffer.null?.should be_true + end + end + + context "with a block" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = +"test" + IO::Buffer.for(string) do |buffer| + buffer.set_string("meat") + buffer.free + buffer.null?.should be_true + end + string.should == "meat" + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = + IO::Buffer.string(4) do |buffer| + buffer.set_string("meat") + buffer.free + buffer.null?.should be_true + end + string.should == "meat" + end + end + end + + it "can be called repeatedly without an error" do + buffer = IO::Buffer.new(4) + buffer.free + buffer.null?.should be_true + buffer.free + buffer.null?.should be_true + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + buffer = IO::Buffer.new(4) + buffer.locked do + -> { buffer.free }.should raise_error(IO::Buffer::LockedError, "Buffer is locked!") + end + buffer.free + buffer.null?.should be_true + end + + context "with a slice of a buffer" do + it "nullifies the slice, not touching the buffer" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + + slice.free + slice.null?.should be_true + buffer.null?.should be_false + + buffer.free + end + + it "nullifies buffer, invalidating the slice" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + + buffer.free + slice.null?.should be_false + slice.valid?.should be_false + end + end +end diff --git a/spec/ruby/core/io/buffer/initialize_spec.rb b/spec/ruby/core/io/buffer/initialize_spec.rb new file mode 100644 index 0000000000..c86d1e7f1d --- /dev/null +++ b/spec/ruby/core/io/buffer/initialize_spec.rb @@ -0,0 +1,103 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#initialize" do + after :each do + @buffer&.free + @buffer = nil + end + + it "creates a new zero-filled buffer with default size" do + @buffer = IO::Buffer.new + @buffer.size.should == IO::Buffer::DEFAULT_SIZE + @buffer.each(:U8).should.all? { |_offset, value| value.eql?(0) } + end + + it "creates a buffer with default state" do + @buffer = IO::Buffer.new + @buffer.should_not.shared? + @buffer.should_not.readonly? + + @buffer.should_not.empty? + @buffer.should_not.null? + + # This is run-time state, set by #locked. + @buffer.should_not.locked? + end + + context "with size argument" do + it "creates a new internal buffer if size is less than IO::Buffer::PAGE_SIZE" do + size = IO::Buffer::PAGE_SIZE - 1 + @buffer = IO::Buffer.new(size) + @buffer.size.should == size + @buffer.should.internal? + @buffer.should_not.mapped? + @buffer.should_not.empty? + end + + it "creates a new mapped buffer if size is greater than or equal to IO::Buffer::PAGE_SIZE" do + size = IO::Buffer::PAGE_SIZE + @buffer = IO::Buffer.new(size) + @buffer.size.should == size + @buffer.should_not.internal? + @buffer.should.mapped? + @buffer.should_not.empty? + end + + it "creates a null buffer if size is 0" do + @buffer = IO::Buffer.new(0) + @buffer.size.should.zero? + @buffer.should_not.internal? + @buffer.should_not.mapped? + @buffer.should.null? + @buffer.should.empty? + end + + it "raises TypeError if size is not an Integer" do + -> { IO::Buffer.new(nil) }.should raise_error(TypeError, "not an Integer") + -> { IO::Buffer.new(10.0) }.should raise_error(TypeError, "not an Integer") + end + + it "raises ArgumentError if size is negative" do + -> { IO::Buffer.new(-1) }.should raise_error(ArgumentError, "Size can't be negative!") + end + end + + context "with size and flags arguments" do + it "forces mapped buffer with IO::Buffer::MAPPED flag" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE - 1, IO::Buffer::MAPPED) + @buffer.should.mapped? + @buffer.should_not.internal? + @buffer.should_not.empty? + end + + it "forces internal buffer with IO::Buffer::INTERNAL flag" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::INTERNAL) + @buffer.should.internal? + @buffer.should_not.mapped? + @buffer.should_not.empty? + end + + it "raises IO::Buffer::AllocationError if neither IO::Buffer::MAPPED nor IO::Buffer::INTERNAL is given" do + -> { IO::Buffer.new(10, IO::Buffer::READONLY) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") + -> { IO::Buffer.new(10, 0) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") + end + + ruby_version_is "3.3" do + it "raises ArgumentError if flags is negative" do + -> { IO::Buffer.new(10, -1) }.should raise_error(ArgumentError, "Flags can't be negative!") + end + end + + ruby_version_is ""..."3.3" do + it "raises IO::Buffer::AllocationError with non-Integer flags" do + -> { IO::Buffer.new(10, 0.0) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") + end + end + + ruby_version_is "3.3" do + it "raises TypeError with non-Integer flags" do + -> { IO::Buffer.new(10, 0.0) }.should raise_error(TypeError, "not an Integer") + end + end + end +end diff --git a/spec/ruby/core/io/buffer/internal_spec.rb b/spec/ruby/core/io/buffer/internal_spec.rb new file mode 100644 index 0000000000..409699cc3c --- /dev/null +++ b/spec/ruby/core/io/buffer/internal_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#internal?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is true for an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.internal?.should be_true + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.internal?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is false for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.internal?.should be_false + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.internal?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.internal?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.internal?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.internal?.should be_false + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.internal?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.internal?.should be_false + end + end + + context "created with .map" do + it "is false" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.internal?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.internal?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.internal?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.internal?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/locked_spec.rb b/spec/ruby/core/io/buffer/locked_spec.rb new file mode 100644 index 0000000000..4ffa569fd2 --- /dev/null +++ b/spec/ruby/core/io/buffer/locked_spec.rb @@ -0,0 +1,75 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#locked" do + after :each do + @buffer&.free + @buffer = nil + end + + context "when buffer is locked" do + it "allows reading and writing operations on the buffer" do + @buffer = IO::Buffer.new(4) + @buffer.set_string("test") + @buffer.locked do + @buffer.get_string.should == "test" + @buffer.set_string("meat") + end + @buffer.get_string.should == "meat" + end + + it "disallows operations changing buffer itself, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + # Just an example, each method is responsible for checking the lock state. + -> { @buffer.resize(8) }.should raise_error(IO::Buffer::LockedError) + end + end + end + + it "disallows reentrant locking, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.locked {} }.should raise_error(IO::Buffer::LockedError, "Buffer already locked!") + end + end + + it "does not propagate to buffer's slices" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.locked do + @buffer.locked?.should be_true + slice.locked?.should be_false + slice.locked { slice.locked?.should be_true } + end + end + + it "does not propagate backwards from buffer's slices" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + slice.locked do + slice.locked?.should be_true + @buffer.locked?.should be_false + @buffer.locked { @buffer.locked?.should be_true } + end + end +end + +describe "IO::Buffer#locked?" do + after :each do + @buffer&.free + @buffer = nil + end + + it "is false by default" do + @buffer = IO::Buffer.new(4) + @buffer.locked?.should be_false + end + + it "is true only inside of #locked block" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + @buffer.locked?.should be_true + end + @buffer.locked?.should be_false + end +end diff --git a/spec/ruby/core/io/buffer/mapped_spec.rb b/spec/ruby/core/io/buffer/mapped_spec.rb new file mode 100644 index 0000000000..b3610207ff --- /dev/null +++ b/spec/ruby/core/io/buffer/mapped_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#mapped?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.mapped?.should be_false + end + + it "is true for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.mapped?.should be_true + end + end + + context "with a file-backed buffer created with .map" do + it "is true for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.mapped?.should be_true + end + end + + ruby_version_is "3.3" do + it "is true for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.mapped?.should be_true + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.mapped?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.mapped?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.mapped?.should be_false + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.mapped?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.mapped?.should be_false + end + end + + context "created with .map" do + it "is false" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.mapped?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.mapped?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.mapped?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.mapped?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/null_spec.rb b/spec/ruby/core/io/buffer/null_spec.rb new file mode 100644 index 0000000000..3fb1144d0e --- /dev/null +++ b/spec/ruby/core/io/buffer/null_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../../spec_helper' +require_relative 'shared/null_and_empty' + +describe "IO::Buffer#null?" do + after :each do + @buffer&.free + @buffer = nil + end + + it_behaves_like :io_buffer_null_and_empty, :null? + + it "is false for a 0-length String-backed buffer created with .for" do + @buffer = IO::Buffer.for("") + @buffer.null?.should be_false + end + + ruby_version_is "3.3" do + it "is false for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.null?.should be_false + end + end + end + + it "is false for a 0-length slice of a buffer with size > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(3, 0).null?.should be_false + end +end diff --git a/spec/ruby/core/io/buffer/private_spec.rb b/spec/ruby/core/io/buffer/private_spec.rb new file mode 100644 index 0000000000..7aa308997b --- /dev/null +++ b/spec/ruby/core/io/buffer/private_spec.rb @@ -0,0 +1,111 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "IO::Buffer#private?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) + @buffer.private?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.private?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is false for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.private?.should be_false + end + end + + it "is true for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.private?.should be_true + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.private?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.private?.should be_false + end + end + end + + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.private?.should be_false + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.private?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.private?.should be_false + end + end + + context "created with .map" do + it "is false when slicing a regular file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.private?.should be_false + end + end + + it "is false when slicing a private file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.slice.private?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.private?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.private?.should be_false + end + end + end + + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.private?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/readonly_spec.rb b/spec/ruby/core/io/buffer/readonly_spec.rb new file mode 100644 index 0000000000..0014a876ed --- /dev/null +++ b/spec/ruby/core/io/buffer/readonly_spec.rb @@ -0,0 +1,143 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#readonly?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) + @buffer.readonly?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.readonly?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is false for a writable mapping" do + File.open(__FILE__, "r+") do |file| + @buffer = IO::Buffer.map(file) + @buffer.readonly?.should be_false + end + end + + it "is true for a readonly mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.readonly?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + @buffer.readonly?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is true for a buffer created without a block" do + @buffer = IO::Buffer.for(+"test") + @buffer.readonly?.should be_true + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.readonly?.should be_false + end + end + + it "is true for a buffer created with a block from a frozen string" do + IO::Buffer.for(-"test") do |buffer| + buffer.readonly?.should be_true + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.readonly?.should be_false + end + end + end + end + + # This seems to be the only flag propagated from the source buffer to the slice. + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.readonly?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.readonly?.should be_false + end + end + + context "created with .map" do + it "is false when slicing a read-write file-backed buffer" do + File.open(__FILE__, "r+") do |file| + @buffer = IO::Buffer.map(file) + @buffer.slice.readonly?.should be_false + end + end + + it "is true when slicing a readonly file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.readonly?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false when slicing a private file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + @buffer.slice.readonly?.should be_false + end + end + end + end + + context "created with .for" do + it "is true when slicing a buffer created without a block" do + @buffer = IO::Buffer.for(+"test") + @buffer.slice.readonly?.should be_true + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.readonly?.should be_false + end + end + + it "is true when slicing a buffer created with a block from a frozen string" do + IO::Buffer.for(-"test") do |buffer| + buffer.slice.readonly?.should be_true + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.readonly?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb new file mode 100644 index 0000000000..0da3a23356 --- /dev/null +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -0,0 +1,155 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#resize" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "resizes internal buffer, preserving type" do + @buffer = IO::Buffer.new(4) + @buffer.resize(IO::Buffer::PAGE_SIZE) + @buffer.size.should == IO::Buffer::PAGE_SIZE + @buffer.internal?.should be_true + @buffer.mapped?.should be_false + end + + platform_is :linux do + it "resizes mapped buffer, preserving type" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED) + @buffer.resize(4) + @buffer.size.should == 4 + @buffer.internal?.should be_false + @buffer.mapped?.should be_true + end + end + + platform_is_not :linux do + it "resizes mapped buffer, changing type to internal" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED) + @buffer.resize(4) + @buffer.size.should == 4 + @buffer.internal?.should be_true + @buffer.mapped?.should be_false + end + end + end + + context "with a file-backed buffer created with .map" do + it "disallows resizing shared buffer, raising IO::Buffer::AccessError" do + File.open(__FILE__, "r+") do |file| + @buffer = IO::Buffer.map(file) + -> { @buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + + ruby_version_is "3.3" do + it "resizes private buffer, discarding excess contents" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + @buffer.resize(10) + @buffer.size.should == 10 + @buffer.get_string.should == "require_re" + @buffer.resize(12) + @buffer.size.should == 12 + @buffer.get_string.should == "require_re\0\0" + end + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "disallows resizing, raising IO::Buffer::AccessError" do + @buffer = IO::Buffer.for(+"test") + -> { @buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + + context "with a block" do + it "disallows resizing, raising IO::Buffer::AccessError" do + IO::Buffer.for(+'test') do |buffer| + -> { buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "disallows resizing, raising IO::Buffer::AccessError" do + IO::Buffer.string(4) do |buffer| + -> { buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + end + end + + context "with a null buffer" do + it "allows resizing a 0-sized buffer, creating a regular buffer according to new size" do + @buffer = IO::Buffer.new(0) + @buffer.resize(IO::Buffer::PAGE_SIZE) + @buffer.size.should == IO::Buffer::PAGE_SIZE + @buffer.internal?.should be_false + @buffer.mapped?.should be_true + end + + it "allows resizing after a free, creating a regular buffer according to new size" do + @buffer = IO::Buffer.for("test") + @buffer.free + @buffer.resize(10) + @buffer.size.should == 10 + @buffer.internal?.should be_true + @buffer.mapped?.should be_false + end + end + + it "allows resizing to 0, freeing memory" do + @buffer = IO::Buffer.new(4) + @buffer.resize(0) + @buffer.null?.should be_true + end + + it "can be called repeatedly" do + @buffer = IO::Buffer.new(4) + @buffer.resize(10) + @buffer.resize(27) + @buffer.resize(1) + @buffer.size.should == 1 + end + + it "always clears extra memory" do + @buffer = IO::Buffer.new(4) + @buffer.set_string("test") + # This should not cause a re-allocation, just a technical resizing, + # even with very aggressive memory allocation. + @buffer.resize(2) + @buffer.resize(4) + @buffer.get_string.should == "te\0\0" + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.resize(10) }.should raise_error(IO::Buffer::LockedError, "Cannot resize locked buffer!") + end + end + + it "raises ArgumentError if size is negative" do + @buffer = IO::Buffer.new(4) + -> { @buffer.resize(-1) }.should raise_error(ArgumentError, "Size can't be negative!") + end + + it "raises TypeError if size is not an Integer" do + @buffer = IO::Buffer.new(4) + -> { @buffer.resize(nil) }.should raise_error(TypeError, "not an Integer") + -> { @buffer.resize(10.0) }.should raise_error(TypeError, "not an Integer") + end + + context "with a slice of a buffer" do + # Current behavior of slice resizing seems unintended (it's undocumented, too). + # It either creates a completely new buffer, or breaks the slice on size 0. + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/io/buffer/shared/null_and_empty.rb b/spec/ruby/core/io/buffer/shared/null_and_empty.rb new file mode 100644 index 0000000000..c8fe9e5e46 --- /dev/null +++ b/spec/ruby/core/io/buffer/shared/null_and_empty.rb @@ -0,0 +1,59 @@ +describe :io_buffer_null_and_empty, shared: true do + it "is false for a buffer with size > 0" do + @buffer = IO::Buffer.new(1) + @buffer.send(@method).should be_false + end + + it "is false for a slice with length > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(1, 2).send(@method).should be_false + end + + it "is false for a file-mapped buffer" do + File.open(__FILE__, "rb") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.send(@method).should be_false + end + end + + it "is false for a non-empty String-backed buffer created with .for" do + @buffer = IO::Buffer.for("test") + @buffer.send(@method).should be_false + end + + ruby_version_is "3.3" do + it "is false for a non-empty String-backed buffer created with .string" do + IO::Buffer.string(4) do |buffer| + buffer.send(@method).should be_false + end + end + end + + it "is true for a 0-sized buffer" do + @buffer = IO::Buffer.new(0) + @buffer.send(@method).should be_true + end + + it "is true for a slice of a 0-sized buffer" do + @buffer = IO::Buffer.new(0) + @buffer.slice(0, 0).send(@method).should be_true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(1) + @buffer.free + @buffer.send(@method).should be_true + end + + it "is true for a buffer resized to 0" do + @buffer = IO::Buffer.new(1) + @buffer.resize(0) + @buffer.send(@method).should be_true + end + + it "is true for a buffer whose memory was transferred" do + buffer = IO::Buffer.new(1) + @buffer = buffer.transfer + buffer.send(@method).should be_true + end +end diff --git a/spec/ruby/core/io/buffer/shared_spec.rb b/spec/ruby/core/io/buffer/shared_spec.rb new file mode 100644 index 0000000000..f2a638cf39 --- /dev/null +++ b/spec/ruby/core/io/buffer/shared_spec.rb @@ -0,0 +1,117 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#shared?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) + @buffer.shared?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.shared?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is true for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.shared?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.shared?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.shared?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.shared?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.shared?.should be_false + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.shared?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.shared?.should be_false + end + end + + context "created with .map" do + it "is false when slicing a regular file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.shared?.should be_false + end + end + + ruby_version_is "3.3" do + it "is false when slicing a private file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.slice.shared?.should be_false + end + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.shared?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.shared?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.shared?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/transfer_spec.rb b/spec/ruby/core/io/buffer/transfer_spec.rb new file mode 100644 index 0000000000..cb8c843ff2 --- /dev/null +++ b/spec/ruby/core/io/buffer/transfer_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#transfer" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "transfers internal memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.new(4) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + + it "transfers mapped memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + end + + context "with a file-backed buffer created with .map" do + it "transfers mapped memory to a new buffer, nullifying the original" do + File.open(__FILE__, "r") do |file| + buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "transfers memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.for("test") + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + end + + context "with a block" do + it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do + IO::Buffer.for(+"test") do |buffer| + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + @buffer.null?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do + IO::Buffer.string(4) do |buffer| + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + @buffer.null?.should be_false + end + end + end + + it "allows multiple transfers" do + buffer_1 = IO::Buffer.new(4) + buffer_2 = buffer_1.transfer + @buffer = buffer_2.transfer + buffer_1.null?.should be_true + buffer_2.null?.should be_true + @buffer.null?.should be_false + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.transfer }.should raise_error(IO::Buffer::LockedError, "Cannot transfer ownership of locked buffer!") + end + end + + context "with a slice of a buffer" do + it "transfers source to a new slice, not touching the buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.set_string("test") + + new_slice = slice.transfer + slice.null?.should be_true + new_slice.null?.should be_false + @buffer.null?.should be_false + + new_slice.set_string("ea") + @buffer.get_string.should == "east" + end + + it "nullifies buffer, invalidating the slice" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + @buffer = buffer.transfer + + slice.null?.should be_false + slice.valid?.should be_false + -> { slice.get_string }.should raise_error(IO::Buffer::InvalidatedError, "Buffer has been invalidated!") + end + end +end diff --git a/spec/ruby/core/io/buffer/valid_spec.rb b/spec/ruby/core/io/buffer/valid_spec.rb new file mode 100644 index 0000000000..680a35ae9a --- /dev/null +++ b/spec/ruby/core/io/buffer/valid_spec.rb @@ -0,0 +1,110 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#valid?" do + after :each do + @buffer&.free + @buffer = nil + end + + # Non-slices are always valid + context "with a non-slice buffer" do + it "is true for a regular buffer" do + @buffer = IO::Buffer.new(4) + @buffer.valid?.should be_true + end + + it "is true for a 0-size buffer" do + @buffer = IO::Buffer.new(0) + @buffer.valid?.should be_true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(4) + @buffer.free + @buffer.valid?.should be_true + end + + it "is true for a freed file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.valid?.should be_true + @buffer.free + @buffer.valid?.should be_true + end + end + + it "is true for a freed string-backed buffer" do + @buffer = IO::Buffer.for("hello") + @buffer.valid?.should be_true + @buffer.free + @buffer.valid?.should be_true + end + end + + # "A buffer becomes invalid if it is a slice of another buffer (or string) + # which has been freed or re-allocated at a different address." + context "with a slice" do + it "is true for a slice of a live buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + slice.valid?.should be_true + end + + context "when buffer is resized" do + it "is false when slice becomes outside the buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(2, 2) + @buffer.resize(3) + slice.valid?.should be_false + end + + platform_is_not :linux do + # This test does not cause a copy-resize on Linux. + # `#resize` MAY cause the buffer to move, but there is no guarantee. + it "is false when buffer is copied on resize" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + slice = @buffer.slice(0, 2) + @buffer.resize(8) + slice.valid?.should be_false + end + end + end + + it "is false for a slice of a transferred buffer" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + @buffer = buffer.transfer + slice.valid?.should be_false + end + + it "is false for a slice of a freed buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.free + slice.valid?.should be_false + end + + it "is false for a slice of a freed file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + slice = @buffer.slice(0, 2) + slice.valid?.should be_true + @buffer.free + slice.valid?.should be_false + end + end + + it "is true for a slice of a freed string-backed buffer while string is alive" do + @buffer = IO::Buffer.for("alive") + slice = @buffer.slice(0, 2) + slice.valid?.should be_true + @buffer.free + slice.valid?.should be_true + end + + # There probably should be a test with a garbage-collected string, + # but it's not clear how to force that. + + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/io/close_on_exec_spec.rb b/spec/ruby/core/io/close_on_exec_spec.rb new file mode 100644 index 0000000000..4e89e08d61 --- /dev/null +++ b/spec/ruby/core/io/close_on_exec_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' + +describe "IO#close_on_exec=" do + before :each do + @name = tmp('io_close_on_exec.txt') + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + guard -> { platform_is_not :windows } do + it "sets the close-on-exec flag if true" do + @io.close_on_exec = true + @io.should.close_on_exec? + end + + it "sets the close-on-exec flag if non-false" do + @io.close_on_exec = :true + @io.should.close_on_exec? + end + + it "unsets the close-on-exec flag if false" do + @io.close_on_exec = true + @io.close_on_exec = false + @io.should_not.close_on_exec? + end + + it "unsets the close-on-exec flag if nil" do + @io.close_on_exec = true + @io.close_on_exec = nil + @io.should_not.close_on_exec? + end + + it "ensures the IO's file descriptor is closed in exec'ed processes" do + require 'fcntl' + @io.close_on_exec = true + (@io.fcntl(Fcntl::F_GETFD) & Fcntl::FD_CLOEXEC).should == Fcntl::FD_CLOEXEC + end + + it "raises IOError if called on a closed IO" do + @io.close + -> { @io.close_on_exec = true }.should raise_error(IOError) + end + end +end + +describe "IO#close_on_exec?" do + before :each do + @name = tmp('io_is_close_on_exec.txt') + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + guard -> { platform_is_not :windows } do + it "returns true by default" do + @io.should.close_on_exec? + end + + it "returns true if set" do + @io.close_on_exec = true + @io.should.close_on_exec? + end + + it "raises IOError if called on a closed IO" do + @io.close + -> { @io.close_on_exec? }.should raise_error(IOError) + end + end +end diff --git a/spec/ruby/core/io/close_read_spec.rb b/spec/ruby/core/io/close_read_spec.rb new file mode 100644 index 0000000000..e700e85bd9 --- /dev/null +++ b/spec/ruby/core/io/close_read_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#close_read" do + + before :each do + cmd = platform_is(:windows) ? 'rem' : 'cat' + @io = IO.popen cmd, "r+" + @path = tmp('io.close.txt') + end + + after :each do + @io.close unless @io.closed? + rm_r @path + end + + it "closes the read end of a duplex I/O stream" do + @io.close_read + + -> { @io.read }.should raise_error(IOError) + end + + it "does nothing on subsequent invocations" do + @io.close_read + + @io.close_read.should be_nil + end + + it "allows subsequent invocation of close" do + @io.close_read + + -> { @io.close }.should_not raise_error + end + + it "raises an IOError if the stream is writable and not duplexed" do + io = File.open @path, 'w' + + begin + -> { io.close_read }.should raise_error(IOError) + ensure + io.close unless io.closed? + end + end + + it "closes the stream if it is neither writable nor duplexed" do + io_close_path = @path + touch io_close_path + + io = File.open io_close_path + + io.close_read + + io.should.closed? + end + + it "does nothing on closed stream" do + @io.close + + @io.close_read.should be_nil + end +end diff --git a/spec/ruby/core/io/close_spec.rb b/spec/ruby/core/io/close_spec.rb new file mode 100644 index 0000000000..3a44cc8b17 --- /dev/null +++ b/spec/ruby/core/io/close_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#close" do + before :each do + @name = tmp('io_close.txt') + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "closes the stream" do + @io.close + @io.should.closed? + end + + it "returns nil" do + @io.close.should == nil + end + + it "raises an IOError reading from a closed IO" do + @io.close + -> { @io.read }.should raise_error(IOError) + end + + it "raises an IOError writing to a closed IO" do + @io.close + -> { @io.write "data" }.should raise_error(IOError) + end + + it 'does not close the stream if autoclose is false' do + other_io = IO.new(@io.fileno) + other_io.autoclose = false + other_io.close + -> { @io.write "data" }.should_not raise_error(IOError) + end + + it "does nothing if already closed" do + @io.close + + @io.close.should be_nil + end + + it "does not call the #flush method but flushes the stream internally" do + @io.should_not_receive(:flush) + @io.close + @io.should.closed? + end + + it 'raises an IOError with a clear message' do + matching_exception = nil + + -> do + IOSpecs::THREAD_CLOSE_RETRIES.times do + read_io, write_io = IO.pipe + going_to_read = false + + thread = Thread.new do + begin + going_to_read = true + read_io.read + rescue IOError => ioe + if ioe.message == IOSpecs::THREAD_CLOSE_ERROR_MESSAGE + matching_exception = ioe + end + # try again + end + end + + # best attempt to ensure the thread is actually blocked on read + Thread.pass until going_to_read && thread.stop? + sleep(0.001) + + read_io.close + thread.join + write_io.close + + matching_exception&.tap {|ex| raise ex} + end + end.should raise_error(IOError, IOSpecs::THREAD_CLOSE_ERROR_MESSAGE) + end +end + +describe "IO#close on an IO.popen stream" do + + it "clears #pid" do + io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r' + + io.pid.should_not == 0 + + io.close + + -> { io.pid }.should raise_error(IOError) + end + + it "sets $?" do + io = IO.popen ruby_cmd('exit 0'), 'r' + io.close + + $?.exitstatus.should == 0 + + io = IO.popen ruby_cmd('exit 1'), 'r' + io.close + + $?.exitstatus.should == 1 + end + + it "waits for the child to exit" do + io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r' + io.close + + $?.exitstatus.should_not == 0 + end + +end diff --git a/spec/ruby/core/io/close_write_spec.rb b/spec/ruby/core/io/close_write_spec.rb new file mode 100644 index 0000000000..70610a3e9d --- /dev/null +++ b/spec/ruby/core/io/close_write_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#close_write" do + before :each do + cmd = platform_is(:windows) ? 'rem' : 'cat' + @io = IO.popen cmd, 'r+' + @path = tmp('io.close.txt') + end + + after :each do + @io.close unless @io.closed? + rm_r @path + end + + it "closes the write end of a duplex I/O stream" do + @io.close_write + + -> { @io.write "attempt to write" }.should raise_error(IOError) + end + + it "does nothing on subsequent invocations" do + @io.close_write + + @io.close_write.should be_nil + end + + it "allows subsequent invocation of close" do + @io.close_write + + -> { @io.close }.should_not raise_error + end + + it "raises an IOError if the stream is readable and not duplexed" do + io = File.open @path, 'w+' + + begin + -> { io.close_write }.should raise_error(IOError) + ensure + io.close unless io.closed? + end + end + + it "closes the stream if it is neither readable nor duplexed" do + io = File.open @path, 'w' + + io.close_write + + io.should.closed? + end + + # Windows didn't have command like cat + platform_is_not :windows do + it "flushes and closes the write stream" do + @io.puts '12345' + + @io.close_write + + @io.read.should == "12345\n" + end + end + + it "does nothing on closed stream" do + @io.close + + @io.close_write.should be_nil + end +end diff --git a/spec/ruby/core/io/closed_spec.rb b/spec/ruby/core/io/closed_spec.rb new file mode 100644 index 0000000000..7316546a0d --- /dev/null +++ b/spec/ruby/core/io/closed_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#closed?" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close + end + + it "returns true on closed stream" do + IOSpecs.closed_io.closed?.should be_true + end + + it "returns false on open stream" do + @io.closed?.should be_false + end +end diff --git a/spec/ruby/core/io/constants_spec.rb b/spec/ruby/core/io/constants_spec.rb new file mode 100644 index 0000000000..f9dccd08b9 --- /dev/null +++ b/spec/ruby/core/io/constants_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "IO::SEEK_SET" do + it "is defined" do + IO.const_defined?(:SEEK_SET).should == true + end +end + +describe "IO::SEEK_CUR" do + it "is defined" do + IO.const_defined?(:SEEK_CUR).should == true + end +end + +describe "IO::SEEK_END" do + it "is defined" do + IO.const_defined?(:SEEK_END).should == true + end +end diff --git a/spec/ruby/core/io/copy_stream_spec.rb b/spec/ruby/core/io/copy_stream_spec.rb new file mode 100644 index 0000000000..ffa2ea992c --- /dev/null +++ b/spec/ruby/core/io/copy_stream_spec.rb @@ -0,0 +1,343 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_copy_stream_to_file, shared: true do + it "copies the entire IO contents to the file" do + IO.copy_stream(@object.from, @to_name) + File.read(@to_name).should == @content + IO.copy_stream(@from_bigfile, @to_name) + File.read(@to_name).should == @content_bigfile + end + + it "returns the number of bytes copied" do + IO.copy_stream(@object.from, @to_name).should == @size + IO.copy_stream(@from_bigfile, @to_name).should == @size_bigfile + end + + it "copies only length bytes when specified" do + IO.copy_stream(@object.from, @to_name, 8).should == 8 + File.read(@to_name).should == "Line one" + end + + it "calls #to_path to convert on object to a file name" do + obj = mock("io_copy_stream_to") + obj.should_receive(:to_path).and_return(@to_name) + + IO.copy_stream(@object.from, obj) + File.read(@to_name).should == @content + end + + it "raises a TypeError if #to_path does not return a String" do + obj = mock("io_copy_stream_to") + obj.should_receive(:to_path).and_return(1) + + -> { IO.copy_stream(@object.from, obj) }.should raise_error(TypeError) + end +end + +describe :io_copy_stream_to_file_with_offset, shared: true do + platform_is_not :windows do + it "copies only length bytes from the offset" do + IO.copy_stream(@object.from, @to_name, 8, 4).should == 8 + File.read(@to_name).should == " one\n\nLi" + end + end +end + +describe :io_copy_stream_to_io, shared: true do + it "copies the entire IO contents to the IO" do + IO.copy_stream(@object.from, @to_io) + File.read(@to_name).should == @content + IO.copy_stream(@from_bigfile, @to_io) + File.read(@to_name).should == (@content + @content_bigfile) + end + + it "returns the number of bytes copied" do + IO.copy_stream(@object.from, @to_io).should == @size + IO.copy_stream(@from_bigfile, @to_io).should == @size_bigfile + end + + it "starts writing at the destination IO's current position" do + @to_io.write("prelude ") + IO.copy_stream(@object.from, @to_io) + File.read(@to_name).should == ("prelude " + @content) + end + + it "leaves the destination IO position at the last write" do + IO.copy_stream(@object.from, @to_io) + @to_io.pos.should == @size + end + + it "raises an IOError if the destination IO is not open for writing" do + to_io = new_io __FILE__, "r" + begin + -> { IO.copy_stream @object.from, to_io }.should raise_error(IOError) + ensure + to_io.close + end + end + + it "does not close the destination IO" do + IO.copy_stream(@object.from, @to_io) + @to_io.closed?.should be_false + end + + it "copies only length bytes when specified" do + IO.copy_stream(@object.from, @to_io, 8).should == 8 + File.read(@to_name).should == "Line one" + end +end + +describe :io_copy_stream_to_io_with_offset, shared: true do + platform_is_not :windows do + it "copies only length bytes from the offset" do + IO.copy_stream(@object.from, @to_io, 8, 4).should == 8 + File.read(@to_name).should == " one\n\nLi" + end + end +end + +describe "IO.copy_stream" do + before :each do + @from_name = fixture __FILE__, "copy_stream.txt" + @to_name = tmp("io_copy_stream_io_name") + + @content = IO.read(@from_name) + @size = @content.size + + @from_bigfile = tmp("io_copy_stream_bigfile") + @content_bigfile = "A" * 17_000 + touch(@from_bigfile){|f| f.print @content_bigfile } + @size_bigfile = @content_bigfile.size + end + + after :each do + rm_r @to_name if @to_name + rm_r @from_bigfile + end + + describe "from an IO" do + before :each do + @from_io = new_io @from_name, "rb" + IOSpecs::CopyStream.from = @from_io + end + + after :each do + @from_io.close + end + + it "raises an IOError if the source IO is not open for reading" do + @from_io.close + @from_io = new_io @from_bigfile, "a" + -> { IO.copy_stream @from_io, @to_name }.should raise_error(IOError) + end + + it "does not close the source IO" do + IO.copy_stream(@from_io, @to_name) + @from_io.closed?.should be_false + end + + platform_is_not :windows do + it "does not change the IO offset when an offset is specified" do + @from_io.pos = 10 + IO.copy_stream(@from_io, @to_name, 8, 4) + @from_io.pos.should == 10 + end + end + + it "does change the IO offset when an offset is not specified" do + @from_io.pos = 10 + IO.copy_stream(@from_io, @to_name) + @from_io.pos.should == 42 + end + + describe "to a file name" do + it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream + end + + describe "to an IO" do + before :each do + @to_io = new_io @to_name, "wb" + end + + after :each do + @to_io.close + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream + end + + describe "to a Tempfile" do + before :all do + require 'tempfile' + end + + before :each do + @to_io = Tempfile.new("rubyspec_copy_stream", encoding: Encoding::BINARY, mode: File::RDONLY) + @to_name = @to_io.path + end + + after :each do + @to_io.close! + @to_name = nil # do not rm_r it, already done by Tempfile#close! + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream + end + end + + describe "from a file name" do + before :each do + IOSpecs::CopyStream.from = @from_name + end + + it "calls #to_path to convert on object to a file name" do + obj = mock("io_copy_stream_from") + obj.should_receive(:to_path).and_return(@from_name) + + IO.copy_stream(obj, @to_name) + File.read(@to_name).should == @content + end + + it "raises a TypeError if #to_path does not return a String" do + obj = mock("io_copy_stream_from") + obj.should_receive(:to_path).and_return(1) + + -> { IO.copy_stream(obj, @to_name) }.should raise_error(TypeError) + end + + describe "to a file name" do + it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream + end + + describe "to an IO" do + before :each do + @to_io = new_io @to_name, "wb" + end + + after :each do + @to_io.close + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream + end + end + + describe "from a pipe IO" do + before :each do + @from_io = IOSpecs.pipe_fixture(@content) + IOSpecs::CopyStream.from = @from_io + end + + after :each do + @from_io.close + end + + it "does not close the source IO" do + IO.copy_stream(@from_io, @to_name) + @from_io.closed?.should be_false + end + + platform_is_not :windows do + it "raises an error when an offset is specified" do + -> { IO.copy_stream(@from_io, @to_name, 8, 4) }.should raise_error(Errno::ESPIPE) + end + end + + describe "to a file name" do + it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream + end + + describe "to an IO" do + before :each do + @to_io = new_io @to_name, "wb" + end + + after :each do + @to_io.close + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + end + end + + describe "with non-IO Objects" do + before do + @io = new_io @from_name, "rb" + end + + after do + @io.close unless @io.closed? + end + + it "calls #readpartial on the source Object if defined" do + from = IOSpecs::CopyStreamReadPartial.new @io + + IO.copy_stream(from, @to_name) + File.read(@to_name).should == @content + end + + it "calls #read on the source Object" do + from = IOSpecs::CopyStreamRead.new @io + + IO.copy_stream(from, @to_name) + File.read(@to_name).should == @content + end + + it "calls #write on the destination Object" do + to = mock("io_copy_stream_to_object") + to.should_receive(:write).with(@content).and_return(@content.size) + + IO.copy_stream(@from_name, to) + end + + it "does not call #pos on the source if no offset is given" do + @io.should_not_receive(:pos) + IO.copy_stream(@io, @to_name) + end + end + + describe "with a destination that does partial reads" do + before do + @from_out, @from_in = IO.pipe + @to_out, @to_in = IO.pipe + end + + after do + [@from_out, @from_in, @to_out, @to_in].each {|io| io.close rescue nil} + end + + it "calls #write repeatedly on the destination Object" do + @from_in.write "1234" + @from_in.close + + th = Thread.new do + IO.copy_stream(@from_out, @to_in) + end + + copied = "" + 4.times do + copied += @to_out.read(1) + end + + th.join + + copied.should == "1234" + end + + end +end + +describe "IO.copy_stream" do + it "does not use buffering when writing to STDOUT" do + IO.popen([*ruby_exe, fixture(__FILE__ , "copy_in_out.rb")], "r+") do |io| + io.write("bar") + io.read(3).should == "bar" + end + end +end diff --git a/spec/ruby/core/io/dup_spec.rb b/spec/ruby/core/io/dup_spec.rb new file mode 100644 index 0000000000..564e007438 --- /dev/null +++ b/spec/ruby/core/io/dup_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#dup" do + before :each do + @file = tmp("spec_io_dup") + @f = File.open @file, 'w+' + @i = @f.dup + + @f.sync = true + @i.sync = true + end + + after :each do + @i.close if @i && !@i.closed? + @f.close if @f && !@f.closed? + rm_r @file + end + + it "returns a new IO instance" do + @i.class.should == @f.class + end + + it "sets a new descriptor on the returned object" do + @i.fileno.should_not == @f.fileno + end + + quarantine! do # This does not appear to be consistent across platforms + it "shares the original stream between the two IOs" do + start = @f.pos + @i.pos.should == start + + s = "Hello, wo.. wait, where am I?\n" + s2 = "<evil voice> Muhahahaa!" + + @f.write s + @i.pos.should == @f.pos + + @i.rewind + @i.gets.should == s + + @i.rewind + @i.write s2 + + @f.rewind + @f.gets.should == "#{s2}\n" + end + end + + it "allows closing the new IO without affecting the original" do + @i.close + -> { @f.gets }.should_not raise_error(Exception) + + @i.should.closed? + @f.should_not.closed? + end + + it "allows closing the original IO without affecting the new one" do + @f.close + -> { @i.gets }.should_not raise_error(Exception) + + @i.should_not.closed? + @f.should.closed? + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.dup }.should raise_error(IOError) + end + + it "always sets the close-on-exec flag for the new IO object" do + @f.close_on_exec = true + dup = @f.dup + begin + dup.should.close_on_exec? + ensure + dup.close + end + + @f.close_on_exec = false + dup = @f.dup + begin + dup.should.close_on_exec? + ensure + dup.close + end + end + + it "always sets the autoclose flag for the new IO object" do + @f.autoclose = true + dup = @f.dup + begin + dup.should.autoclose? + ensure + dup.close + end + + @f.autoclose = false + dup = @f.dup + begin + dup.should.autoclose? + ensure + dup.close + @f.autoclose = true + end + end +end diff --git a/spec/ruby/core/io/each_byte_spec.rb b/spec/ruby/core/io/each_byte_spec.rb new file mode 100644 index 0000000000..ea618e8c0c --- /dev/null +++ b/spec/ruby/core/io/each_byte_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#each_byte" do + before :each do + ScratchPad.record [] + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.each_byte {} }.should raise_error(IOError) + end + + it "yields each byte" do + count = 0 + @io.each_byte do |byte| + ScratchPad << byte + break if 4 < count += 1 + end + + ScratchPad.recorded.should == [86, 111, 105, 99, 105] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.each_byte + enum.should be_an_instance_of(Enumerator) + enum.first(5).should == [86, 111, 105, 99, 105] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.each_byte.size.should == nil + end + end + end + end +end + +describe "IO#each_byte" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns self on an empty stream" do + @io.each_byte { |b| }.should equal(@io) + end +end diff --git a/spec/ruby/core/io/each_char_spec.rb b/spec/ruby/core/io/each_char_spec.rb new file mode 100644 index 0000000000..5d460a1e7c --- /dev/null +++ b/spec/ruby/core/io/each_char_spec.rb @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/chars' + +describe "IO#each_char" do + it_behaves_like :io_chars, :each_char +end + +describe "IO#each_char" do + it_behaves_like :io_chars_empty, :each_char +end diff --git a/spec/ruby/core/io/each_codepoint_spec.rb b/spec/ruby/core/io/each_codepoint_spec.rb new file mode 100644 index 0000000000..07a4037c8a --- /dev/null +++ b/spec/ruby/core/io/each_codepoint_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/codepoints' + +# See redmine #1667 +describe "IO#each_codepoint" do + it_behaves_like :io_codepoints, :each_codepoint +end + +describe "IO#each_codepoint" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "calls the given block" do + r = [] + @io.each_codepoint { |c| r << c } + r[24].should == 232 + r.last.should == 10 + end + + it "returns self" do + @io.each_codepoint { |l| l }.should equal(@io) + end +end + +describe "IO#each_codepoint" do + before :each do + @io = IOSpecs.io_fixture("incomplete.txt") + end + + after :each do + @io.close if @io + end + + it "raises an exception at incomplete character before EOF when conversion takes place" do + -> { @io.each_codepoint {} }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/each_line_spec.rb b/spec/ruby/core/io/each_line_spec.rb new file mode 100644 index 0000000000..58d26b325d --- /dev/null +++ b/spec/ruby/core/io/each_line_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/each' + +describe "IO#each_line" do + it_behaves_like :io_each, :each_line +end + +describe "IO#each_line" do + it_behaves_like :io_each_default_separator, :each_line +end diff --git a/spec/ruby/core/io/each_spec.rb b/spec/ruby/core/io/each_spec.rb new file mode 100644 index 0000000000..91ecbd19c8 --- /dev/null +++ b/spec/ruby/core/io/each_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/each' + +describe "IO#each" do + it_behaves_like :io_each, :each +end + +describe "IO#each" do + it_behaves_like :io_each_default_separator, :each +end diff --git a/spec/ruby/core/io/eof_spec.rb b/spec/ruby/core/io/eof_spec.rb new file mode 100644 index 0000000000..b4850df437 --- /dev/null +++ b/spec/ruby/core/io/eof_spec.rb @@ -0,0 +1,107 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#eof?" do + before :each do + @name = tmp("empty.txt") + touch @name + end + + after :each do + rm_r @name + end + + it "returns true on an empty stream that has just been opened" do + File.open(@name) { |empty| empty.should.eof? } + end + + it "raises IOError on stream not opened for reading" do + -> do + File.open(@name, "w") { |f| f.eof? } + end.should raise_error(IOError) + end +end + +describe "IO#eof?" do + before :each do + @name = fixture __FILE__, "lines.txt" + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io && !@io.closed? + end + + it "returns false when not at end of file" do + @io.read 1 + @io.should_not.eof? + end + + it "returns true after reading with read with no parameters" do + @io.read() + @io.should.eof? + end + + it "returns true after reading with read" do + @io.read(File.size(@name)) + @io.should.eof? + end + + it "returns true after reading with sysread" do + @io.sysread(File.size(@name)) + @io.should.eof? + end + + it "returns true after reading with readlines" do + @io.readlines + @io.should.eof? + end + + it "returns false on just opened non-empty stream" do + @io.should_not.eof? + end + + it "does not consume the data from the stream" do + @io.should_not.eof? + @io.getc.should == 'V' + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.eof? }.should raise_error(IOError) + end + + it "raises IOError on stream closed for reading by close_read" do + @io.close_read + -> { @io.eof? }.should raise_error(IOError) + end + + it "returns true on one-byte stream after single-byte read" do + File.open(__dir__ + '/fixtures/one_byte.txt') { |one_byte| + one_byte.read(1) + one_byte.should.eof? + } + end +end + +describe "IO#eof?" do + after :each do + @r.close if @r && !@r.closed? + @w.close if @w && !@w.closed? + end + + it "returns true on receiving side of Pipe when writing side is closed" do + @r, @w = IO.pipe + @w.close + @r.should.eof? + end + + it "returns false on receiving side of Pipe when writing side wrote some data" do + @r, @w = IO.pipe + @w.puts "hello" + @r.should_not.eof? + @w.close + @r.should_not.eof? + @r.read + @r.should.eof? + end +end diff --git a/spec/ruby/core/io/external_encoding_spec.rb b/spec/ruby/core/io/external_encoding_spec.rb new file mode 100644 index 0000000000..7765c6c0f5 --- /dev/null +++ b/spec/ruby/core/io/external_encoding_spec.rb @@ -0,0 +1,223 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_external_encoding_write, shared: true do + describe "when Encoding.default_internal is nil" do + before :each do + Encoding.default_internal = nil + end + + it "returns nil" do + @io = new_io @name, @object + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should be_nil + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "#{@object}:ibm866" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "#{@object}:ibm866" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external != Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_external when the instance was created" do + @io = new_io @name, @object + Encoding.default_external = Encoding::UTF_8 + @io.external_encoding.should equal(Encoding::IBM437) + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "#{@object}:ibm866" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "#{@object}:ibm866" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external == Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM866 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_external when the instance was created" do + @io = new_io @name, @object + Encoding.default_external = Encoding::UTF_8 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "#{@object}:ibm866" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "#{@object}:ibm866" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end +end + +describe "IO#external_encoding" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + @name = tmp("io_external_encoding") + touch(@name) + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + + @io.close if @io + rm_r @name + end + + it "can be retrieved from a closed stream" do + io = IOSpecs.io_fixture("lines.txt", "r") + io.close + io.external_encoding.should equal(Encoding.default_external) + end + + describe "with 'r' mode" do + describe "when Encoding.default_internal is nil" do + before :each do + Encoding.default_internal = nil + Encoding.default_external = Encoding::IBM866 + end + + it "returns Encoding.default_external if the external encoding is not set" do + @io = new_io @name, "r" + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns Encoding.default_external when that encoding is changed after the instance is created" do + @io = new_io @name, "r" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM437) + 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 + @io.external_encoding.should equal(Encoding::UTF_8) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "r:utf-8" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external == Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM866 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_external when the instance was created" do + @io = new_io @name, "r" + 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 + @io.external_encoding.should equal(Encoding::UTF_8) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "r:utf-8" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external != Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = 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 + @io.external_encoding.should equal(Encoding::UTF_8) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "r:utf-8" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + end + + describe "with 'rb' mode" do + it "returns Encoding::BINARY" do + @io = new_io @name, "rb" + @io.external_encoding.should equal(Encoding::BINARY) + end + + it "returns the external encoding specified by the mode argument" do + @io = new_io @name, "rb:ibm437" + @io.external_encoding.should equal(Encoding::IBM437) + end + end + + describe "with 'r+' mode" do + it_behaves_like :io_external_encoding_write, nil, "r+" + end + + describe "with 'w' mode" do + it_behaves_like :io_external_encoding_write, nil, "w" + end + + describe "with 'wb' mode" do + it "returns Encoding::BINARY" do + @io = new_io @name, "wb" + @io.external_encoding.should equal(Encoding::BINARY) + end + + it "returns the external encoding specified by the mode argument" do + @io = new_io @name, "wb:ibm437" + @io.external_encoding.should equal(Encoding::IBM437) + end + end + + describe "with 'w+' mode" do + it_behaves_like :io_external_encoding_write, nil, "w+" + end + + describe "with 'a' mode" do + it_behaves_like :io_external_encoding_write, nil, "a" + end + + describe "with 'a+' mode" do + it_behaves_like :io_external_encoding_write, nil, "a+" + end +end diff --git a/spec/ruby/core/io/fcntl_spec.rb b/spec/ruby/core/io/fcntl_spec.rb new file mode 100644 index 0000000000..30b4876fe3 --- /dev/null +++ b/spec/ruby/core/io/fcntl_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#fcntl" do + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.fcntl(5, 5) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/fdatasync_spec.rb b/spec/ruby/core/io/fdatasync_spec.rb new file mode 100644 index 0000000000..6242258ea0 --- /dev/null +++ b/spec/ruby/core/io/fdatasync_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "IO#fdatasync" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/io/fileno_spec.rb b/spec/ruby/core/io/fileno_spec.rb new file mode 100644 index 0000000000..647609bf42 --- /dev/null +++ b/spec/ruby/core/io/fileno_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#fileno" do + it "returns the numeric file descriptor of the given IO object" do + $stdout.fileno.should == 1 + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.fileno }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt b/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt Binary files differnew file mode 100644 index 0000000000..c7c42e9de4 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt b/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt Binary files differnew file mode 100644 index 0000000000..53642b6984 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt b/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt Binary files differnew file mode 100644 index 0000000000..c5efe6c278 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt b/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt Binary files differnew file mode 100644 index 0000000000..1168384393 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-8.txt b/spec/ruby/core/io/fixtures/bom_UTF-8.txt new file mode 100644 index 0000000000..ca971bef61 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-8.txt @@ -0,0 +1 @@ +UTF-8 diff --git a/spec/ruby/core/io/fixtures/classes.rb b/spec/ruby/core/io/fixtures/classes.rb new file mode 100644 index 0000000000..204a2a101b --- /dev/null +++ b/spec/ruby/core/io/fixtures/classes.rb @@ -0,0 +1,218 @@ +# -*- encoding: utf-8 -*- + +module IOSpecs + THREAD_CLOSE_RETRIES = 10 + THREAD_CLOSE_ERROR_MESSAGE = 'stream closed in another thread' + + class SubIO < IO + end + + class SubIOWithRedefinedNew < IO + def self.new(...) + ScratchPad << :redefined_new_called + super + end + + def initialize(...) + ScratchPad << :call_original_initialize + super + end + end + + def self.collector + Proc.new { |x| ScratchPad << x } + end + + def self.lines + [ "Voici la ligne une.\n", + "Qui \303\250 la linea due.\n", + "\n", + "\n", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\n", + "Hier ist Zeile vier.\n", + "\n", + "Est\303\241 aqui a linha cinco.\n", + "Here is line six.\n" ] + end + + def self.lines_without_newline_characters + [ "Voici la ligne une.", + "Qui \303\250 la linea due.", + "", + "", + "Aqu\303\255 est\303\241 la l\303\255nea tres.", + "Hier ist Zeile vier.", + "", + "Est\303\241 aqui a linha cinco.", + "Here is line six." ] + end + + def self.lines_limit + [ "Voici la l", + "igne une.\n", + "Qui è la ", + "linea due.", + "\n", + "\n", + "\n", + "Aquí está", + " la línea", + " tres.\n", + "Hier ist Z", + "eile vier.", + "\n", + "\n", + "Está aqui", + " a linha c", + "inco.\n", + "Here is li", + "ne six.\n" ] + end + + def self.lines_space_separator_limit + [ "Voici ", + "la ", + "ligne ", + "une.\nQui ", + "è ", + "la ", + "linea ", + "due.\n\n\nAqu", + "í ", + "está ", + "la ", + "línea ", + "tres.\nHier", + " ", + "ist ", + "Zeile ", + "vier.\n\nEst", + "á ", + "aqui ", + "a ", + "linha ", + "cinco.\nHer", + "e ", + "is ", + "line ", + "six.\n" ] + end + + def self.lines_r_separator + [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n\nAqu\303\255 est\303\241 la l\303\255nea tr", + "es.\nHier", + " ist Zeile vier", + ".\n\nEst\303\241 aqui a linha cinco.\nHer", + "e is line six.\n" ] + end + + def self.lines_empty_separator + [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\n", + "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + def self.lines_space_separator + [ "Voici ", "la ", "ligne ", "une.\nQui ", + "\303\250 ", "la ", "linea ", "due.\n\n\nAqu\303\255 ", + "est\303\241 ", "la ", "l\303\255nea ", "tres.\nHier ", + "ist ", "Zeile ", "vier.\n\nEst\303\241 ", "aqui ", "a ", + "linha ", "cinco.\nHere ", "is ", "line ", "six.\n" ] + end + + def self.lines_space_separator_without_trailing_spaces + [ "Voici", "la", "ligne", "une.\nQui", + "\303\250", "la", "linea", "due.\n\n\nAqu\303\255", + "est\303\241", "la", "l\303\255nea", "tres.\nHier", + "ist", "Zeile", "vier.\n\nEst\303\241", "aqui", "a", + "linha", "cinco.\nHere", "is", "line", "six.\n" ] + end + + def self.lines_arbitrary_separator + [ "Voici la ligne une.\nQui \303\250", + " la linea due.\n\n\nAqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\nEst\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + def self.paragraphs + [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\n", + "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + def self.paragraphs_without_trailing_new_line_characters + [ "Voici la ligne une.\nQui \303\250 la linea due.", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.", + "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + # Creates an IO instance for an existing fixture file. The + # file should obviously not be deleted. + def self.io_fixture(name, mode = "r:utf-8") + path = fixture __FILE__, name + name = path if File.exist? path + new_io(name, mode) + end + + # Returns a closed instance of IO that was opened to reference + # a fixture file (i.e. the IO instance was perfectly valid at + # one point but is now closed). + def self.closed_io + io = io_fixture "lines.txt" + io.close + io + end + + # Creates a pipe-based IO fixture containing the specified + # contents + def self.pipe_fixture(content) + source, sink = IO.pipe + sink.write content + sink.close + source + end + + # Defines +method+ on +obj+ using the provided +block+. This + # special helper is needed for e.g. IO.open specs to avoid + # mock methods preventing IO#close from running. + def self.io_mock(obj, method, &block) + obj.singleton_class.send(:define_method, method, &block) + end + + module CopyStream + def self.from=(from) + @from = from + end + + def self.from + @from + end + end + + class CopyStreamRead + def initialize(io) + @io = io + end + + def read(size, buf) + @io.read size, buf + end + + def send(*args) + raise "send called" + end + end + + class CopyStreamReadPartial + def initialize(io) + @io = io + end + + def readpartial(size, buf) + @io.readpartial size, buf + end + + def send(*args) + raise "send called" + end + end +end diff --git a/spec/ruby/core/io/fixtures/copy_in_out.rb b/spec/ruby/core/io/fixtures/copy_in_out.rb new file mode 100644 index 0000000000..b9d4085a47 --- /dev/null +++ b/spec/ruby/core/io/fixtures/copy_in_out.rb @@ -0,0 +1,2 @@ +STDOUT.sync = false +IO.copy_stream(STDIN, STDOUT) diff --git a/spec/ruby/core/io/fixtures/copy_stream.txt b/spec/ruby/core/io/fixtures/copy_stream.txt new file mode 100644 index 0000000000..a2d827b351 --- /dev/null +++ b/spec/ruby/core/io/fixtures/copy_stream.txt @@ -0,0 +1,6 @@ +Line one + +Line three +Line four + +Line last diff --git a/spec/ruby/core/io/fixtures/empty.txt b/spec/ruby/core/io/fixtures/empty.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/spec/ruby/core/io/fixtures/empty.txt diff --git a/spec/ruby/core/io/fixtures/incomplete.txt b/spec/ruby/core/io/fixtures/incomplete.txt new file mode 100644 index 0000000000..23d432f642 --- /dev/null +++ b/spec/ruby/core/io/fixtures/incomplete.txt @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/spec/ruby/core/io/fixtures/lines.txt b/spec/ruby/core/io/fixtures/lines.txt new file mode 100644 index 0000000000..0959997e7b --- /dev/null +++ b/spec/ruby/core/io/fixtures/lines.txt @@ -0,0 +1,9 @@ +Voici la ligne une. +Qui è la linea due. + + +Aquí está la línea tres. +Hier ist Zeile vier. + +Está aqui a linha cinco. +Here is line six. diff --git a/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt b/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt new file mode 100644 index 0000000000..7edc66b06a --- /dev/null +++ b/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt @@ -0,0 +1 @@ +UTF-8 diff --git a/spec/ruby/core/io/fixtures/numbered_lines.txt b/spec/ruby/core/io/fixtures/numbered_lines.txt new file mode 100644 index 0000000000..70e49a3d98 --- /dev/null +++ b/spec/ruby/core/io/fixtures/numbered_lines.txt @@ -0,0 +1,5 @@ +Line 1: One +Line 2: Two +Line 3: Three +Line 4: Four +Line 5: Five diff --git a/spec/ruby/core/io/fixtures/one_byte.txt b/spec/ruby/core/io/fixtures/one_byte.txt new file mode 100644 index 0000000000..56a6051ca2 --- /dev/null +++ b/spec/ruby/core/io/fixtures/one_byte.txt @@ -0,0 +1 @@ +1
\ No newline at end of file diff --git a/spec/ruby/core/io/fixtures/read_binary.txt b/spec/ruby/core/io/fixtures/read_binary.txt new file mode 100644 index 0000000000..fa036dca4b --- /dev/null +++ b/spec/ruby/core/io/fixtures/read_binary.txt @@ -0,0 +1 @@ +abcdef diff --git a/spec/ruby/core/io/fixtures/read_euc_jp.txt b/spec/ruby/core/io/fixtures/read_euc_jp.txt new file mode 100644 index 0000000000..0e17cd717a --- /dev/null +++ b/spec/ruby/core/io/fixtures/read_euc_jp.txt @@ -0,0 +1 @@ +꤬Ȥ diff --git a/spec/ruby/core/io/fixtures/read_text.txt b/spec/ruby/core/io/fixtures/read_text.txt new file mode 100644 index 0000000000..5a7a80f6e4 --- /dev/null +++ b/spec/ruby/core/io/fixtures/read_text.txt @@ -0,0 +1 @@ +abcâdef diff --git a/spec/ruby/core/io/fixtures/reopen_stdout.rb b/spec/ruby/core/io/fixtures/reopen_stdout.rb new file mode 100644 index 0000000000..506ba072bd --- /dev/null +++ b/spec/ruby/core/io/fixtures/reopen_stdout.rb @@ -0,0 +1,3 @@ +STDOUT.reopen ARGV[0] +system "echo from system" +exec "echo from exec" diff --git a/spec/ruby/core/io/flush_spec.rb b/spec/ruby/core/io/flush_spec.rb new file mode 100644 index 0000000000..f7d5ba77fc --- /dev/null +++ b/spec/ruby/core/io/flush_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#flush" do + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.flush }.should raise_error(IOError) + end + + describe "on a pipe" do + before :each do + @r, @w = IO.pipe + end + + after :each do + @r.close + begin + @w.close + rescue Errno::EPIPE + end + end + + # [ruby-core:90895] RJIT worker may leave fd open in a forked child. + # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens, + # and the RJIT worker keeps the pipe open until the worker execve(). + # TODO: consider acquiring GVL from RJIT worker. + guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do + it "raises Errno::EPIPE if sync=false and the read end is closed" do + @w.sync = false + @w.write "foo" + @r.close + + -> { @w.flush }.should raise_error(Errno::EPIPE, /Broken pipe/) + -> { @w.close }.should raise_error(Errno::EPIPE, /Broken pipe/) + end + end + end +end diff --git a/spec/ruby/core/io/for_fd_spec.rb b/spec/ruby/core/io/for_fd_spec.rb new file mode 100644 index 0000000000..2d86361b73 --- /dev/null +++ b/spec/ruby/core/io/for_fd_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/new' + +describe "IO.for_fd" do + it_behaves_like :io_new, :for_fd +end + +describe "IO.for_fd" do + it_behaves_like :io_new_errors, :for_fd +end diff --git a/spec/ruby/core/io/foreach_spec.rb b/spec/ruby/core/io/foreach_spec.rb new file mode 100644 index 0000000000..6abe8901ba --- /dev/null +++ b/spec/ruby/core/io/foreach_spec.rb @@ -0,0 +1,98 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/readlines' + +describe "IO.foreach" do + before :each do + @name = fixture __FILE__, "lines.txt" + @count = 0 + ScratchPad.record [] + end + + it "updates $. with each yield" do + IO.foreach(@name) { $..should == @count += 1 } + end + + ruby_version_is ""..."4.0" do + describe "when the filename starts with |" do + it "gets data from the standard out of the subprocess" do + cmd = "|sh -c 'echo hello;echo line2'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello&echo line2" + end + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.foreach(cmd) { |l| ScratchPad << l } + end + ScratchPad.recorded.should == ["hello\n", "line2\n"] + end + + platform_is_not :windows do + it "gets data from a fork when passed -" do + parent_pid = $$ + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.foreach("|-") { |l| ScratchPad << l } + end + + if $$ == parent_pid + ScratchPad.recorded.should == ["hello\n", "from a fork\n"] + else # child + puts "hello" + puts "from a fork" + exit! + end + end + end + end + + ruby_version_is "3.3" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + cmd = "|echo ok" + -> { + IO.foreach(cmd).to_a + }.should complain(/IO process creation with a leading '\|'/) + end + end + end +end + +describe "IO.foreach" do + before :each do + @external = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + + @name = fixture __FILE__, "lines.txt" + ScratchPad.record [] + end + + after :each do + Encoding.default_external = @external + end + + it "sets $_ to nil" do + $_ = "test" + IO.foreach(@name) { } + $_.should be_nil + end + + describe "when no block is given" do + it "returns an Enumerator" do + IO.foreach(@name).should be_an_instance_of(Enumerator) + IO.foreach(@name).to_a.should == IOSpecs.lines + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + IO.foreach(@name).size.should == nil + end + end + end + end + + it_behaves_like :io_readlines, :foreach, IOSpecs.collector + it_behaves_like :io_readlines_options_19, :foreach, IOSpecs.collector +end diff --git a/spec/ruby/core/io/fsync_spec.rb b/spec/ruby/core/io/fsync_spec.rb new file mode 100644 index 0000000000..6e6123de94 --- /dev/null +++ b/spec/ruby/core/io/fsync_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#fsync" do + before :each do + @name = tmp("io_fsync.txt") + ScratchPad.clear + end + + after :each do + rm_r @name + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.fsync }.should raise_error(IOError) + end + + it "writes the buffered data to permanent storage" do + File.open(@name, "w") do |f| + f.write "one hit wonder" + f.fsync.should == 0 + end + end +end diff --git a/spec/ruby/core/io/getbyte_spec.rb b/spec/ruby/core/io/getbyte_spec.rb new file mode 100644 index 0000000000..b4351160e6 --- /dev/null +++ b/spec/ruby/core/io/getbyte_spec.rb @@ -0,0 +1,58 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#getbyte" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "returns the next byte from the stream" do + @io.readline.should == "Voici la ligne une.\n" + letters = @io.getbyte, @io.getbyte, @io.getbyte, @io.getbyte, @io.getbyte + letters.should == [81, 117, 105, 32, 195] + end + + it "returns nil when invoked at the end of the stream" do + @io.read + @io.getbyte.should == nil + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.getbyte }.should raise_error(IOError) + end +end + +describe "IO#getbyte" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close if @io + end + + it "returns nil on empty stream" do + @io.getbyte.should == nil + end +end + +describe "IO#getbyte" do + before :each do + @name = tmp("io_getbyte.txt") + @io = new_io(@name, 'w') + end + + after :each do + @io.close if @io + rm_r @name if @name + end + + it "raises an IOError if the stream is not readable" do + -> { @io.getbyte }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/getc_spec.rb b/spec/ruby/core/io/getc_spec.rb new file mode 100644 index 0000000000..3949b5cb28 --- /dev/null +++ b/spec/ruby/core/io/getc_spec.rb @@ -0,0 +1,42 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#getc" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "returns the next character from the stream" do + @io.readline.should == "Voici la ligne une.\n" + letters = @io.getc, @io.getc, @io.getc, @io.getc, @io.getc + letters.should == ["Q", "u", "i", " ", "è"] + end + + it "returns nil when invoked at the end of the stream" do + @io.read + @io.getc.should be_nil + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.getc }.should raise_error(IOError) + end +end + +describe "IO#getc" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close if @io + end + + it "returns nil on empty stream" do + @io.getc.should be_nil + end +end diff --git a/spec/ruby/core/io/gets_spec.rb b/spec/ruby/core/io/gets_spec.rb new file mode 100644 index 0000000000..ca64bf860e --- /dev/null +++ b/spec/ruby/core/io/gets_spec.rb @@ -0,0 +1,360 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/gets_ascii' + +describe "IO#gets" do + it_behaves_like :io_gets_ascii, :gets +end + +describe "IO#gets" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @count = 0 + end + + after :each do + @io.close if @io + end + + it "assigns the returned line to $_" do + IOSpecs.lines.each do |line| + @io.gets + $_.should == line + end + end + + it "sets $_ to nil after the last line has been read" do + while @io.gets + end + $_.should be_nil + end + + it "returns nil if called at the end of the stream" do + IOSpecs.lines.length.times { @io.gets } + @io.gets.should == nil + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.gets }.should raise_error(IOError) + end + + describe "with no separator" do + it "returns the next line of string that is separated by $/" do + IOSpecs.lines.each { |line| line.should == @io.gets } + end + + it "updates lineno with each invocation" do + while @io.gets + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets + $..should == @count += 1 + end + end + end + + describe "with nil separator" do + it "returns the entire contents" do + @io.gets(nil).should == IOSpecs.lines.join("") + end + + it "updates lineno with each invocation" do + while @io.gets(nil) + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets(nil) + $..should == @count += 1 + end + end + end + + describe "with an empty String separator" do + # Two successive newlines in the input separate paragraphs. + # When there are more than two successive newlines, only two are kept. + it "returns the next paragraph" do + @io.gets("").should == IOSpecs.lines[0,3].join("") + @io.gets("").should == IOSpecs.lines[4,3].join("") + @io.gets("").should == IOSpecs.lines[7,2].join("") + end + + it "reads until the beginning of the next paragraph" do + # There are three newlines between the first and second paragraph + @io.gets("") + @io.gets.should == IOSpecs.lines[4] + end + + it "updates lineno with each invocation" do + while @io.gets("") + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets("") + $..should == @count += 1 + end + end + end + + describe "with an arbitrary String separator" do + it "reads up to and including the separator" do + @io.gets("la linea").should == "Voici la ligne une.\nQui \303\250 la linea" + end + + it "updates lineno with each invocation" do + while (@io.gets("la")) + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets("la") + $..should == @count += 1 + end + end + + describe "that consists of multiple bytes" do + platform_is_not :windows do + it "should match the separator even if the buffer is filled over successive reads" do + IO.pipe do |read, write| + + # Write part of the string with the separator split between two write calls. We want + # the read to intertwine such that when the read starts the full data isn't yet + # available in the buffer. + write.write("Aquí está la línea tres\r\n") + + t = Thread.new do + # Continue reading until the separator is encountered or the pipe is closed. + read.gets("\r\n\r\n") + end + + # Write the other half of the separator, which should cause the `gets` call to now + # match. Explicitly close the pipe for good measure so a bug in `gets` doesn't block forever. + Thread.pass until t.stop? + + write.write("\r\nelse\r\n\r\n") + write.close + + t.value.bytes.should == "Aquí está la línea tres\r\n\r\n".bytes + read.read(8).bytes.should == "else\r\n\r\n".bytes + end + end + end + end + end + + describe "when passed chomp" do + it "returns the first line without a trailing newline character" do + @io.gets(chomp: true).should == IOSpecs.lines_without_newline_characters[0] + end + + it "raises exception when options passed as Hash" do + -> { @io.gets({ chomp: true }) }.should raise_error(TypeError) + + -> { + @io.gets("\n", 1, { chomp: true }) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end +end + +describe "IO#gets" do + before :each do + @name = tmp("io_gets") + end + + after :each do + rm_r @name + end + + it "raises an IOError if the stream is opened for append only" do + -> { File.open(@name, "a:utf-8") { |f| f.gets } }.should raise_error(IOError) + end + + it "raises an IOError if the stream is opened for writing only" do + -> { File.open(@name, "w:utf-8") { |f| f.gets } }.should raise_error(IOError) + end +end + +describe "IO#gets" do + before :each do + @name = tmp("io_gets") + touch(@name) { |f| f.write "one\n\ntwo\n\nthree\nfour\n" } + @io = new_io @name, "r:utf-8" + end + + after :each do + @io.close if @io + rm_r @name + end + + it "calls #to_int to convert a single object argument to an Integer limit" do + obj = mock("io gets limit") + obj.should_receive(:to_int).and_return(6) + + @io.gets(obj).should == "one\n" + end + + it "calls #to_int to convert the second object argument to an Integer limit" do + obj = mock("io gets limit") + obj.should_receive(:to_int).and_return(2) + + @io.gets(nil, obj).should == "on" + end + + it "calls #to_str to convert the first argument to a String when passed a limit" do + obj = mock("io gets separator") + obj.should_receive(:to_str).and_return($/) + + @io.gets(obj, 5).should == "one\n" + end + + it "reads to the default separator when passed a single argument greater than the number of bytes to the separator" do + @io.gets(6).should == "one\n" + end + + it "reads limit bytes when passed a single argument less than the number of bytes to the default separator" do + @io.gets(3).should == "one" + end + + it "reads limit bytes when passed nil and a limit" do + @io.gets(nil, 6).should == "one\n\nt" + end + + it "reads all bytes when the limit is higher than the available bytes" do + @io.gets(nil, 100).should == "one\n\ntwo\n\nthree\nfour\n" + end + + it "reads until the next paragraph when passed '' and a limit greater than the next paragraph" do + @io.gets("", 6).should == "one\n\n" + end + + it "reads limit bytes when passed '' and a limit less than the next paragraph" do + @io.gets("", 3).should == "one" + end + + it "reads all bytes when pass a separator and reading more than all bytes" do + @io.gets("\t", 100).should == "one\n\ntwo\n\nthree\nfour\n" + end + + it "returns empty string when 0 passed as a limit" do + @io.gets(0).should == "" + @io.gets(nil, 0).should == "" + @io.gets("", 0).should == "" + end + + it "does not accept limit that doesn't fit in a C off_t" do + -> { @io.gets(2**128) }.should raise_error(RangeError) + end +end + +describe "IO#gets" do + before :each do + @name = tmp("io_gets") + # create data "朝日" + "\xE3\x81" * 100 to avoid utf-8 conflicts + data = "朝日" + ([227,129].pack('C*') * 100).force_encoding('utf-8') + touch(@name) { |f| f.write data } + @io = new_io @name, "r:utf-8" + end + + after :each do + @io.close if @io + rm_r @name + end + + it "reads limit bytes and extra bytes when limit is reached not at character boundary" do + [@io.gets(1), @io.gets(1)].should == ["朝", "日"] + end + + it "read limit bytes and extra bytes with maximum of 16" do + # create str "朝日\xE3" + "\x81\xE3" * 8 to avoid utf-8 conflicts + str = "朝日" + ([227] + [129,227] * 8).pack('C*').force_encoding('utf-8') + @io.gets(7).should == str + end +end + +describe "IO#gets" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil + + @name = tmp("io_gets") + touch(@name) { |f| f.write "line" } + end + + after :each do + @io.close if @io + rm_r @name + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "uses the default external encoding" do + @io = new_io @name, 'r' + @io.gets.encoding.should == Encoding::UTF_8 + end + + it "uses the IO object's external encoding, when set" do + @io = new_io @name, 'r' + @io.set_encoding Encoding::US_ASCII + @io.gets.encoding.should == Encoding::US_ASCII + end + + it "transcodes into the default internal encoding" do + Encoding.default_internal = Encoding::US_ASCII + @io = new_io @name, 'r' + @io.gets.encoding.should == Encoding::US_ASCII + end + + it "transcodes into the IO object's internal encoding, when set" do + Encoding.default_internal = Encoding::US_ASCII + @io = new_io @name, 'r' + @io.set_encoding Encoding::UTF_8, Encoding::UTF_16 + @io.gets.encoding.should == Encoding::UTF_16 + end + + it "overwrites the default external encoding with the IO object's own external encoding" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.set_encoding Encoding::IBM866 + @io.gets.encoding.should == Encoding::UTF_8 + end + + it "ignores the internal encoding if the default external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.gets.encoding.should == Encoding::BINARY + end + + ruby_version_is ''...'3.3' do + it "transcodes to internal encoding if the IO object's external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.set_encoding Encoding::BINARY, Encoding::UTF_8 + @io.gets.encoding.should == Encoding::UTF_8 + end + end + + ruby_version_is '3.3' do + it "ignores the internal encoding if the IO object's external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.set_encoding Encoding::BINARY, Encoding::UTF_8 + @io.gets.encoding.should == Encoding::BINARY + end + end +end diff --git a/spec/ruby/core/io/initialize_spec.rb b/spec/ruby/core/io/initialize_spec.rb new file mode 100644 index 0000000000..026252a13d --- /dev/null +++ b/spec/ruby/core/io/initialize_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#initialize" do + before :each do + @name = tmp("io_initialize.txt") + @io = IO.new(new_fd(@name)) + @fd = @io.fileno + end + + after :each do + @io.close if @io + rm_r @name + end + + it "reassociates the IO instance with the new descriptor when passed an Integer" do + fd = new_fd @name, "r:utf-8" + @io.send :initialize, fd, 'r' + @io.fileno.should == fd + end + + it "calls #to_int to coerce the object passed as an fd" do + obj = mock('fileno') + fd = new_fd @name, "r:utf-8" + obj.should_receive(:to_int).and_return(fd) + @io.send :initialize, obj, 'r' + @io.fileno.should == fd + end + + it "accepts options as keyword arguments" do + fd = new_fd @name, "w:utf-8" + + @io.send(:initialize, fd, "w", flags: File::CREAT) + @io.fileno.should == fd + + -> { + @io.send(:initialize, fd, "w", {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 1..2)") + end + + it "raises a TypeError when passed an IO" do + -> { @io.send :initialize, STDOUT, 'w' }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + -> { @io.send :initialize, nil, 'w' }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { @io.send :initialize, "4", 'w' }.should raise_error(TypeError) + end + + it "raises IOError on closed stream" do + -> { @io.send :initialize, IOSpecs.closed_io.fileno }.should raise_error(IOError) + end + + it "raises an Errno::EBADF when given an invalid file descriptor" do + -> { @io.send :initialize, -1, 'w' }.should raise_error(Errno::EBADF) + end +end diff --git a/spec/ruby/core/io/inspect_spec.rb b/spec/ruby/core/io/inspect_spec.rb new file mode 100644 index 0000000000..c653c307c4 --- /dev/null +++ b/spec/ruby/core/io/inspect_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "IO#inspect" do + after :each do + @r.close if @r && !@r.closed? + @w.close if @w && !@w.closed? + end + + it "contains the file descriptor number" do + @r, @w = IO.pipe + @r.inspect.should include("fd #{@r.fileno}") + end + + it "contains \"(closed)\" if the stream is closed" do + @r, @w = IO.pipe + @r.close + @r.inspect.should include("(closed)") + end + + it "reports IO as its Method object's owner" do + IO.instance_method(:inspect).owner.should == IO + end +end diff --git a/spec/ruby/core/io/internal_encoding_spec.rb b/spec/ruby/core/io/internal_encoding_spec.rb new file mode 100644 index 0000000000..7a583d4bcb --- /dev/null +++ b/spec/ruby/core/io/internal_encoding_spec.rb @@ -0,0 +1,145 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_internal_encoding, shared: true do + describe "when Encoding.default_internal is not set" do + before :each do + Encoding.default_internal = nil + end + + it "returns nil if the internal encoding is not set" do + @io = new_io @name, @object + @io.internal_encoding.should be_nil + end + + it "returns nil if Encoding.default_internal is changed after the instance is created" do + @io = new_io @name, @object + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should be_nil + end + + it "returns the value set when the instance was created" do + @io = new_io @name, "#{@object}:utf-8:euc-jp" + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should equal(Encoding::EUC_JP) + end + + it "returns the value set by #set_encoding" do + @io = new_io @name, @object + @io.set_encoding(Encoding::US_ASCII, Encoding::IBM437) + @io.internal_encoding.should equal(Encoding::IBM437) + end + end + + describe "when Encoding.default_internal == Encoding.default_external" do + before :each do + Encoding.default_external = Encoding::IBM866 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns nil" do + @io = new_io @name, @object + @io.internal_encoding.should be_nil + end + + it "returns nil regardless of Encoding.default_internal changes" do + @io = new_io @name, @object + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should be_nil + end + end + + describe "when Encoding.default_internal != Encoding.default_external" do + before :each do + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_internal when the instance was created if the internal encoding is not set" do + @io = new_io @name, @object + @io.internal_encoding.should equal(Encoding::IBM866) + end + + it "does not change when Encoding.default_internal is changed" do + @io = new_io @name, @object + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should equal(Encoding::IBM866) + end + + it "returns the internal encoding set when the instance was created" do + @io = new_io @name, "#{@object}:utf-8:euc-jp" + @io.internal_encoding.should equal(Encoding::EUC_JP) + end + + it "does not change when set and Encoding.default_internal is changed" do + @io = new_io @name, "#{@object}:utf-8:euc-jp" + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should equal(Encoding::EUC_JP) + end + + it "returns the value set by #set_encoding" do + @io = new_io @name, @object + @io.set_encoding(Encoding::US_ASCII, Encoding::IBM437) + @io.internal_encoding.should equal(Encoding::IBM437) + end + + it "returns nil when Encoding.default_external is BINARY and the internal encoding is not set" do + Encoding.default_external = Encoding::BINARY + @io = new_io @name, @object + @io.internal_encoding.should be_nil + end + + it "returns nil when the external encoding is BINARY and the internal encoding is not set" do + @io = new_io @name, "#{@object}:binary" + @io.internal_encoding.should be_nil + end + end +end + +describe "IO#internal_encoding" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + @name = tmp("io_internal_encoding") + touch(@name) + end + + after :each do + @io.close if @io + rm_r @name + + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "can be retrieved from a closed stream" do + io = IOSpecs.io_fixture("lines.txt", "r") + io.close + io.internal_encoding.should equal(Encoding.default_internal) + end + + describe "with 'r' mode" do + it_behaves_like :io_internal_encoding, nil, "r" + end + + describe "with 'r+' mode" do + it_behaves_like :io_internal_encoding, nil, "r+" + end + + describe "with 'w' mode" do + it_behaves_like :io_internal_encoding, nil, "w" + end + + describe "with 'w+' mode" do + it_behaves_like :io_internal_encoding, nil, "w+" + end + + describe "with 'a' mode" do + it_behaves_like :io_internal_encoding, nil, "a" + end + + describe "with 'a+' mode" do + it_behaves_like :io_internal_encoding, nil, "a+" + end +end diff --git a/spec/ruby/core/io/io_spec.rb b/spec/ruby/core/io/io_spec.rb new file mode 100644 index 0000000000..0feb1a8774 --- /dev/null +++ b/spec/ruby/core/io/io_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "IO" do + it "includes File::Constants" do + IO.include?(File::Constants).should == true + end + + it "includes Enumerable" do + IO.include?(Enumerable).should == true + end +end diff --git a/spec/ruby/core/io/ioctl_spec.rb b/spec/ruby/core/io/ioctl_spec.rb new file mode 100644 index 0000000000..3f7b5ad5d7 --- /dev/null +++ b/spec/ruby/core/io/ioctl_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#ioctl" do + platform_is_not :windows do + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.ioctl(5, 5) }.should raise_error(IOError) + end + end + + platform_is :linux do + guard -> { RUBY_PLATFORM.include?("86") } do # x86 / x86_64 + it "resizes an empty String to match the output size" do + File.open(__FILE__, 'r') do |f| + buffer = +'' + # FIONREAD in /usr/include/asm-generic/ioctls.h + f.ioctl 0x541B, buffer + buffer.unpack('I').first.should be_kind_of(Integer) + end + end + end + + it "raises a system call error when ioctl fails" do + File.open(__FILE__, 'r') do |f| + -> { + # TIOCGWINSZ in /usr/include/asm-generic/ioctls.h + f.ioctl 0x5413, nil + }.should raise_error(SystemCallError) + end + end + end +end diff --git a/spec/ruby/core/io/isatty_spec.rb b/spec/ruby/core/io/isatty_spec.rb new file mode 100644 index 0000000000..3b6c69b190 --- /dev/null +++ b/spec/ruby/core/io/isatty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/tty' + +describe "IO#isatty" do + it_behaves_like :io_tty, :isatty +end diff --git a/spec/ruby/core/io/lineno_spec.rb b/spec/ruby/core/io/lineno_spec.rb new file mode 100644 index 0000000000..e82cdd9f17 --- /dev/null +++ b/spec/ruby/core/io/lineno_spec.rb @@ -0,0 +1,138 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#lineno" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "raises an IOError on a closed stream" do + -> { IOSpecs.closed_io.lineno }.should raise_error(IOError) + end + + it "raises an IOError on a write-only stream" do + name = tmp("io_lineno.txt") + begin + File.open(name, 'w') do |f| + -> { f.lineno }.should raise_error(IOError) + end + ensure + rm_r name + end + end + + it "raises an IOError on a duplexed stream with the read side closed" do + cmd = platform_is(:windows) ? 'rem' : 'cat' + IO.popen(cmd, 'r+') do |p| + p.close_read + -> { p.lineno }.should raise_error(IOError) + end + end + + it "returns the current line number" do + @io.lineno.should == 0 + + count = 0 + while @io.gets + @io.lineno.should == count += 1 + end + + @io.rewind + @io.lineno.should == 0 + end +end + +describe "IO#lineno=" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "raises an IOError on a closed stream" do + -> { IOSpecs.closed_io.lineno = 5 }.should raise_error(IOError) + end + + it "raises an IOError on a write-only stream" do + name = tmp("io_lineno.txt") + begin + File.open(name, 'w') do |f| + -> { f.lineno = 0 }.should raise_error(IOError) + end + ensure + rm_r name + end + end + + it "raises an IOError on a duplexed stream with the read side closed" do + cmd = platform_is(:windows) ? 'rem' : 'cat' + IO.popen(cmd, 'r+') do |p| + p.close_read + -> { p.lineno = 0 }.should raise_error(IOError) + end + end + + it "calls #to_int on a non-numeric argument" do + obj = mock('123') + obj.should_receive(:to_int).and_return(123) + + @io.lineno = obj + @io.lineno.should == 123 + end + + it "truncates a Float argument" do + @io.lineno = 1.5 + @io.lineno.should == 1 + + @io.lineno = 92233.72036854775808 + @io.lineno.should == 92233 + end + + it "raises TypeError if cannot convert argument to Integer implicitly" do + -> { @io.lineno = "1" }.should raise_error(TypeError, 'no implicit conversion of String into Integer') + -> { @io.lineno = nil }.should raise_error(TypeError, 'no implicit conversion from nil to integer') + end + + it "does not accept Integers that don't fit in a C int" do + -> { @io.lineno = 2**32 }.should raise_error(RangeError) + end + + it "sets the current line number to the given value" do + @io.lineno = count = 500 + + while @io.gets + @io.lineno.should == count += 1 + end + + @io.rewind + @io.lineno.should == 0 + end + + it "does not change $." do + original_line = $. + numbers = [-2**30, -2**16, -2**8, -100, -10, -1, 0, 1, 10, 2**8, 2**16, 2**30] + numbers.each do |num| + @io.lineno = num + @io.lineno.should == num + $..should == original_line + end + end + + it "does not change $. until next read" do + $. = 0 + $..should == 0 + + @io.lineno = count = 500 + $..should == 0 + + while @io.gets + $..should == count += 1 + end + end +end diff --git a/spec/ruby/core/io/new_spec.rb b/spec/ruby/core/io/new_spec.rb new file mode 100644 index 0000000000..979ac0efcb --- /dev/null +++ b/spec/ruby/core/io/new_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'shared/new' + +# NOTE: should be synchronized with library/stringio/initialize_spec.rb + +describe "IO.new" do + it_behaves_like :io_new, :new + + it "does not use the given block and warns to use IO::open" do + -> { + @io = IO.send(@method, @fd) { raise } + }.should complain(/warning: IO::new\(\) does not take block; use IO::open\(\) instead/) + end +end + +describe "IO.new" do + it_behaves_like :io_new_errors, :new +end diff --git a/spec/ruby/core/io/nonblock_spec.rb b/spec/ruby/core/io/nonblock_spec.rb new file mode 100644 index 0000000000..99dc0cafd0 --- /dev/null +++ b/spec/ruby/core/io/nonblock_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' + +platform_is_not :windows do + describe "IO#nonblock?" do + before :all do + require 'io/nonblock' + end + + it "returns false for a file by default" do + File.open(__FILE__) do |f| + f.nonblock?.should == false + end + end + + it "returns true for pipe by default" do + r, w = IO.pipe + begin + r.nonblock?.should == true + w.nonblock?.should == true + ensure + r.close + w.close + end + end + + it "returns true for socket by default" do + require 'socket' + TCPServer.open(0) do |socket| + socket.nonblock?.should == true + end + end + end + + describe "IO#nonblock=" do + before :all do + require 'io/nonblock' + end + + it "changes the IO to non-blocking mode" do + File.open(__FILE__) do |f| + f.nonblock = true + f.nonblock?.should == true + f.nonblock = false + f.nonblock?.should == false + end + end + end +end diff --git a/spec/ruby/core/io/open_spec.rb b/spec/ruby/core/io/open_spec.rb new file mode 100644 index 0000000000..d151da9ce5 --- /dev/null +++ b/spec/ruby/core/io/open_spec.rb @@ -0,0 +1,99 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/new' + +describe "IO.open" do + it_behaves_like :io_new, :open +end + +describe "IO.open" do + it_behaves_like :io_new_errors, :open +end + +# These specs use a special mock helper to avoid mock +# methods from preventing IO#close from running and +# which would prevent the file referenced by @fd from +# being deleted on Windows. + +describe "IO.open" do + before :each do + @name = tmp("io_open.txt") + @fd = new_fd @name + ScratchPad.clear + end + + after :each do + rm_r @name + end + + it "calls #close after yielding to the block" do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + end + io.closed?.should be_false + end + ScratchPad.recorded.should == :called + end + + it "propagate an exception in the block after calling #close" do + -> do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + end + raise Exception + end + end.should raise_error(Exception) + ScratchPad.recorded.should == :called + end + + it "propagates an exception raised by #close that is not a StandardError" do + -> do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + raise Exception + end + end + end.should raise_error(Exception) + ScratchPad.recorded.should == :called + end + + it "propagates an exception raised by #close that is a StandardError" do + -> do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + raise StandardError + end + end + end.should raise_error(StandardError) + ScratchPad.recorded.should == :called + end + + it "does not propagate an IOError with 'closed stream' message raised by #close" do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + raise IOError, 'closed stream' + end + end + ScratchPad.recorded.should == :called + end + + it "does not set last error when an IOError with 'closed stream' raised by #close" do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + raise IOError, 'closed stream' + end + end + $!.should == nil + end +end diff --git a/spec/ruby/core/io/output_spec.rb b/spec/ruby/core/io/output_spec.rb new file mode 100644 index 0000000000..2aafb305f4 --- /dev/null +++ b/spec/ruby/core/io/output_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#<<" do + it "writes an object to the IO stream" do + -> { + $stderr << "Oh noes, an error!" + }.should output_to_fd("Oh noes, an error!", $stderr) + end + + it "calls #to_s on the object to print it" do + -> { + $stderr << 1337 + }.should output_to_fd("1337", $stderr) + end + + it "raises an error if the stream is closed" do + io = IOSpecs.closed_io + -> { io << "test" }.should raise_error(IOError) + end + + it "returns self" do + -> { + ($stderr << "to_stderr").should == $stderr + }.should output(nil, "to_stderr") + end +end diff --git a/spec/ruby/core/io/path_spec.rb b/spec/ruby/core/io/path_spec.rb new file mode 100644 index 0000000000..798adb2163 --- /dev/null +++ b/spec/ruby/core/io/path_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "IO#path" do + it "returns the path of the file associated with the IO object" do + path = tmp("io_path.txt") + File.open(path, "w") do |file| + IO.new(file.fileno, path: file.path, autoclose: false).path.should == file.path + end + ensure + File.unlink(path) + end +end diff --git a/spec/ruby/core/io/pid_spec.rb b/spec/ruby/core/io/pid_spec.rb new file mode 100644 index 0000000000..bc09fe7c3b --- /dev/null +++ b/spec/ruby/core/io/pid_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#pid" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "returns nil for IO not associated with a process" do + @io.pid.should == nil + end +end + +describe "IO#pid" do + before :each do + @io = IO.popen ruby_cmd('STDIN.read'), "r+" + end + + after :each do + @io.close if @io && !@io.closed? + end + + it "returns the ID of a process associated with stream" do + @io.pid.should_not be_nil + end + + it "raises an IOError on closed stream" do + @io.close + -> { @io.pid }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/pipe_spec.rb b/spec/ruby/core/io/pipe_spec.rb new file mode 100644 index 0000000000..aee0d9003f --- /dev/null +++ b/spec/ruby/core/io/pipe_spec.rb @@ -0,0 +1,225 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.pipe" do + after :each do + @r.close if @r && !@r.closed? + @w.close if @w && !@w.closed? + end + + it "creates a two-ended pipe" do + @r, @w = IO.pipe + @w.puts "test_create_pipe\\n" + @w.close + @r.read(16).should == "test_create_pipe" + end + + it "returns two IO objects" do + @r, @w = IO.pipe + @r.should be_kind_of(IO) + @w.should be_kind_of(IO) + end + + it "returns instances of a subclass when called on a subclass" do + @r, @w = IOSpecs::SubIO.pipe + @r.should be_an_instance_of(IOSpecs::SubIO) + @w.should be_an_instance_of(IOSpecs::SubIO) + end + + it "does not use IO.new method to create pipes and allows its overriding" do + ScratchPad.record [] + + # so redefined .new is not called, but original #initialize is + @r, @w = IOSpecs::SubIOWithRedefinedNew.pipe + ScratchPad.recorded.should == [:call_original_initialize, :call_original_initialize] # called 2 times - for each pipe (r and w) + + @r.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew) + @w.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew) + end +end + +describe "IO.pipe" do + describe "passed a block" do + it "yields two IO objects" do + IO.pipe do |r, w| + r.should be_kind_of(IO) + w.should be_kind_of(IO) + end + end + + it "returns the result of the block" do + IO.pipe { |r, w| :result }.should == :result + end + + it "closes both IO objects" do + r, w = IO.pipe do |_r, _w| + [_r, _w] + end + r.should.closed? + w.should.closed? + end + + it "closes both IO objects when the block raises" do + r = w = nil + -> do + IO.pipe do |_r, _w| + r = _r + w = _w + raise RuntimeError + end + end.should raise_error(RuntimeError) + r.should.closed? + w.should.closed? + end + + it "allows IO objects to be closed within the block" do + r, w = IO.pipe do |_r, _w| + _r.close + _w.close + [_r, _w] + end + r.should.closed? + w.should.closed? + end + end +end + +describe "IO.pipe" do + before :each do + @default_external = Encoding.default_external + @default_internal = Encoding.default_internal + end + + after :each do + Encoding.default_external = @default_external + Encoding.default_internal = @default_internal + end + + it "sets the external encoding of the read end to the default when passed no arguments" do + Encoding.default_external = Encoding::ISO_8859_1 + + IO.pipe do |r, w| + r.external_encoding.should == Encoding::ISO_8859_1 + r.internal_encoding.should be_nil + end + end + + it "sets the internal encoding of the read end to the default when passed no arguments" do + Encoding.default_external = Encoding::ISO_8859_1 + Encoding.default_internal = Encoding::UTF_8 + + IO.pipe do |r, w| + r.external_encoding.should == Encoding::ISO_8859_1 + r.internal_encoding.should == Encoding::UTF_8 + end + end + + it "sets the internal encoding to nil if the same as the external" do + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = Encoding::UTF_8 + + IO.pipe do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "sets the external encoding of the read end when passed an Encoding argument" do + IO.pipe(Encoding::UTF_8) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "sets the external and internal encodings of the read end when passed two Encoding arguments" do + IO.pipe(Encoding::UTF_8, Encoding::UTF_16BE) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "sets the external encoding of the read end when passed the name of an Encoding" do + IO.pipe("UTF-8") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "accepts 'bom|' prefix for external encoding" do + IO.pipe("BOM|UTF-8") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "sets the external and internal encodings specified as a String and separated with a colon" do + IO.pipe("UTF-8:ISO-8859-1") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "accepts 'bom|' prefix for external encoding when specifying 'external:internal'" do + IO.pipe("BOM|UTF-8:ISO-8859-1") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "sets the external and internal encoding when passed two String arguments" do + IO.pipe("UTF-8", "UTF-16BE") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "accepts an options Hash with one String encoding argument" do + IO.pipe("BOM|UTF-8:ISO-8859-1", invalid: :replace) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "accepts an options Hash with two String encoding arguments" do + IO.pipe("UTF-8", "ISO-8859-1", invalid: :replace) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "calls #to_hash to convert an options argument" do + options = mock("io pipe encoding options") + options.should_receive(:to_hash).and_return({ invalid: :replace }) + IO.pipe("UTF-8", "ISO-8859-1", **options) { |r, w| } + end + + it "calls #to_str to convert the first argument to a String" do + obj = mock("io_pipe_encoding") + obj.should_receive(:to_str).and_return("UTF-8:UTF-16BE") + IO.pipe(obj) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "calls #to_str to convert the second argument to a String" do + obj = mock("io_pipe_encoding") + obj.should_receive(:to_str).at_least(1).times.and_return("UTF-16BE") + IO.pipe(Encoding::UTF_8, obj) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "sets no external encoding for the write end" do + IO.pipe(Encoding::UTF_8) do |r, w| + w.external_encoding.should be_nil + end + end + + it "sets no internal encoding for the write end" do + IO.pipe(Encoding::UTF_8) do |r, w| + w.external_encoding.should be_nil + end + end +end diff --git a/spec/ruby/core/io/popen_spec.rb b/spec/ruby/core/io/popen_spec.rb new file mode 100644 index 0000000000..6043862614 --- /dev/null +++ b/spec/ruby/core/io/popen_spec.rb @@ -0,0 +1,287 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../process/fixtures/common' + +describe "IO.popen" do + ProcessSpecs.use_system_ruby(self) + + before :each do + @fname = tmp("IO_popen_spec") + @io = nil + @var = "$FOO" + platform_is :windows do + @var = "%FOO%" + end + end + + after :each do + @io.close if @io and !@io.closed? + rm_r @fname + end + + it "returns an open IO" do + @io = IO.popen(ruby_cmd('exit'), "r") + @io.closed?.should be_false + end + + it "reads a read-only pipe" do + @io = IO.popen('echo foo', "r") + @io.read.should == "foo\n" + end + + it "raises IOError when writing a read-only pipe" do + @io = IO.popen('echo foo', "r") + -> { @io.write('bar') }.should raise_error(IOError) + @io.read.should == "foo\n" + end + + it "sees an infinitely looping subprocess exit when read pipe is closed" do + io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r' + io.close + + $?.exitstatus.should_not == 0 + end + + it "writes to a write-only pipe" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)', args: "> #{@fname}"), "w") + @io.write("bar") + @io.close + + File.read(@fname).should == "bar" + end + + it "raises IOError when reading a write-only pipe" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)'), "w") + -> { @io.read }.should raise_error(IOError) + end + + it "reads and writes a read/write pipe" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)'), "r+") + @io.write("bar") + @io.read(3).should == "bar" + end + + it "waits for the child to finish" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)', args: "> #{@fname}"), "w") + @io.write("bar") + @io.close + + $?.exitstatus.should == 0 + + File.read(@fname).should == "bar" + end + + it "does not throw an exception if child exited and has been waited for" do + @io = IO.popen([*ruby_exe, '-e', 'sleep']) + pid = @io.pid + Process.kill "KILL", pid + @io.close + platform_is_not :windows do + $?.should.signaled? + end + platform_is :windows do + $?.should.exited? + end + end + + it "returns an instance of a subclass when called on a subclass" do + @io = IOSpecs::SubIO.popen(ruby_cmd('exit'), "r") + @io.should be_an_instance_of(IOSpecs::SubIO) + end + + it "coerces mode argument with #to_str" do + mode = mock("mode") + mode.should_receive(:to_str).and_return("r") + @io = IO.popen(ruby_cmd('exit 0'), mode) + end + + it "accepts a path using the chdir: keyword argument" do + path = File.dirname(@fname) + + @io = IO.popen(ruby_cmd("puts Dir.pwd"), "r", chdir: path) + @io.read.chomp.should == path + end + + it "accepts a path using the chdir: keyword argument and a coercible path" do + path = File.dirname(@fname) + object = mock("path") + object.should_receive(:to_path).and_return(path) + + @io = IO.popen(ruby_cmd("puts Dir.pwd"), "r", chdir: object) + @io.read.chomp.should == path + end + + describe "with a block" do + it "yields an open IO to the block" do + IO.popen(ruby_cmd('exit'), "r") do |io| + io.closed?.should be_false + end + end + + it "yields an instance of a subclass when called on a subclass" do + IOSpecs::SubIO.popen(ruby_cmd('exit'), "r") do |io| + io.should be_an_instance_of(IOSpecs::SubIO) + end + end + + it "closes the IO after yielding" do + io = IO.popen(ruby_cmd('exit'), "r") { |_io| _io } + io.closed?.should be_true + end + + it "allows the IO to be closed inside the block" do + io = IO.popen(ruby_cmd('exit'), 'r') { |_io| _io.close; _io } + io.closed?.should be_true + end + + it "returns the value of the block" do + IO.popen(ruby_cmd('exit'), "r") { :hello }.should == :hello + end + end + + platform_is_not :windows do + it "starts returns a forked process if the command is -" do + io = IO.popen("-") + + if io # parent + begin + io.gets.should == "hello from child\n" + ensure + io.close + end + else # child + puts "hello from child" + exit! + end + end + end + + it "has the given external encoding" do + @io = IO.popen(ruby_cmd('exit'), external_encoding: Encoding::EUC_JP) + @io.external_encoding.should == Encoding::EUC_JP + end + + it "has the given internal encoding" do + @io = IO.popen(ruby_cmd('exit'), internal_encoding: Encoding::EUC_JP) + @io.internal_encoding.should == Encoding::EUC_JP + end + + it "sets the internal encoding to nil if it's the same as the external encoding" do + @io = IO.popen(ruby_cmd('exit'), external_encoding: Encoding::EUC_JP, + internal_encoding: Encoding::EUC_JP) + @io.internal_encoding.should be_nil + end + + context "with a leading ENV Hash" do + it "accepts a single String command" do + IO.popen({"FOO" => "bar"}, "echo #{@var}") do |io| + io.read.should == "bar\n" + end + end + + it "accepts a single String command, and an IO mode" do + IO.popen({"FOO" => "bar"}, "echo #{@var}", "r") do |io| + io.read.should == "bar\n" + end + end + + it "accepts a single String command with a trailing Hash of Process.exec options" do + IO.popen({"FOO" => "bar"}, ruby_cmd('STDERR.puts ENV["FOO"]'), + err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts a single String command with a trailing Hash of Process.exec options, and an IO mode" do + IO.popen({"FOO" => "bar"}, ruby_cmd('STDERR.puts ENV["FOO"]'), "r", + err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array of command and arguments" do + exe, *args = ruby_exe + IO.popen({"FOO" => "bar"}, [[exe, "specfu"], *args, "-e", "puts ENV['FOO']"]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array of command and arguments, and an IO mode" do + exe, *args = ruby_exe + IO.popen({"FOO" => "bar"}, [[exe, "specfu"], *args, "-e", "puts ENV['FOO']"], "r") do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array command with a separate trailing Hash of Process.exec options" do + IO.popen({"FOO" => "bar"}, [*ruby_exe, "-e", "STDERR.puts ENV['FOO']"], + err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array command with a separate trailing Hash of Process.exec options, and an IO mode" do + IO.popen({"FOO" => "bar"}, [*ruby_exe, "-e", "STDERR.puts ENV['FOO']"], + "r", err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + end + + context "with a leading Array argument" do + it "uses the Array as command plus args for the child process" do + IO.popen([*ruby_exe, "-e", "puts 'hello'"]) do |io| + io.read.should == "hello\n" + end + end + + it "accepts a leading ENV Hash" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "puts ENV['FOO']"]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts a trailing Hash of Process.exec options" do + IO.popen([*ruby_exe, "does_not_exist", {err: [:child, :out]}]) do |io| + io.read.should =~ /LoadError/ + end + end + + it "accepts an IO mode argument following the Array" do + IO.popen([*ruby_exe, "does_not_exist", {err: [:child, :out]}], "r") do |io| + io.read.should =~ /LoadError/ + end + end + + it "accepts [env, command, arg1, arg2, ..., exec options]" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]", + err: [:child, :out]]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts '[env, command, arg1, arg2, ..., exec options], mode'" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]", + err: [:child, :out]], "r") do |io| + io.read.should == "bar\n" + end + end + + it "accepts '[env, command, arg1, arg2, ..., exec options], mode, IO options'" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]", + err: [:child, :out]], "r", + internal_encoding: Encoding::EUC_JP) do |io| + io.read.should == "bar\n" + io.internal_encoding.should == Encoding::EUC_JP + end + end + + it "accepts '[env, command, arg1, arg2, ...], mode, IO + exec options'" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]"], "r", + err: [:child, :out], internal_encoding: Encoding::EUC_JP) do |io| + io.read.should == "bar\n" + io.internal_encoding.should == Encoding::EUC_JP + end + end + end +end diff --git a/spec/ruby/core/io/pos_spec.rb b/spec/ruby/core/io/pos_spec.rb new file mode 100644 index 0000000000..e6cda2643d --- /dev/null +++ b/spec/ruby/core/io/pos_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#pos" do + it_behaves_like :io_pos, :pos +end + +describe "IO#pos=" do + it_behaves_like :io_set_pos, :pos= +end diff --git a/spec/ruby/core/io/pread_spec.rb b/spec/ruby/core/io/pread_spec.rb new file mode 100644 index 0000000000..dc7bcedf3e --- /dev/null +++ b/spec/ruby/core/io/pread_spec.rb @@ -0,0 +1,140 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +guard -> { platform_is_not :windows or ruby_version_is "3.3" } do + describe "IO#pread" do + before :each do + @fname = tmp("io_pread.txt") + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + @file = File.open(@fname, "r+") + end + + after :each do + @file.close + rm_r @fname + end + + it "accepts a length, and an offset" do + @file.pread(4, 0).should == "1234" + @file.pread(3, 4).should == "567" + end + + it "accepts a length, an offset, and an output buffer" do + buffer = +"foo" + @file.pread(3, 4, buffer).should.equal?(buffer) + buffer.should == "567" + end + + it "shrinks the buffer in case of less bytes read" do + buffer = +"foo" + @file.pread(1, 0, buffer) + buffer.should == "1" + end + + it "grows the buffer in case of more bytes read" do + buffer = +"foo" + @file.pread(5, 0, buffer) + buffer.should == "12345" + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @file.pread(10, 0, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end + + it "does not advance the file pointer" do + @file.pread(4, 0).should == "1234" + @file.read.should == "1234567890" + end + + it "ignores the current offset" do + @file.pos = 3 + @file.pread(4, 0).should == "1234" + end + + it "returns an empty string for maxlen = 0" do + @file.pread(0, 4).should == "" + end + + it "returns a buffer for maxlen = 0 when buffer specified" do + buffer = +"foo" + @file.pread(0, 4, buffer).should.equal?(buffer) + buffer.should == "foo" + end + + it "ignores the offset for maxlen = 0, even if it is out of file bounds" do + @file.pread(0, 400).should == "" + end + + it "does not reset the buffer when reading with maxlen = 0" do + buffer = +"foo" + @file.pread(0, 4, buffer) + buffer.should == "foo" + + @file.pread(0, 400, buffer) + buffer.should == "foo" + end + + it "converts maxlen to Integer using #to_int" do + maxlen = mock('maxlen') + maxlen.should_receive(:to_int).and_return(4) + @file.pread(maxlen, 0).should == "1234" + end + + it "converts offset to Integer using #to_int" do + offset = mock('offset') + offset.should_receive(:to_int).and_return(0) + @file.pread(4, offset).should == "1234" + end + + it "converts a buffer to String using to_str" do + buffer = mock('buffer') + buffer.should_receive(:to_str).at_least(1).and_return(+"foo") + @file.pread(4, 0, buffer) + buffer.should_not.is_a?(String) + buffer.to_str.should == "1234" + end + + it "raises TypeError if maxlen is not an Integer and cannot be coerced into Integer" do + maxlen = Object.new + -> { @file.pread(maxlen, 0) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer') + end + + it "raises TypeError if offset is not an Integer and cannot be coerced into Integer" do + offset = Object.new + -> { @file.pread(4, offset) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer') + end + + it "raises ArgumentError for negative values of maxlen" do + -> { @file.pread(-4, 0) }.should raise_error(ArgumentError, 'negative string size (or size too big)') + end + + it "raised Errno::EINVAL for negative values of offset" do + -> { @file.pread(4, -1) }.should raise_error(Errno::EINVAL, /Invalid argument/) + end + + it "raises TypeError if the buffer is not a String and cannot be coerced into String" do + buffer = Object.new + -> { @file.pread(4, 0, buffer) }.should raise_error(TypeError, 'no implicit conversion of Object into String') + end + + it "raises EOFError if end-of-file is reached" do + -> { @file.pread(1, 10) }.should raise_error(EOFError) + end + + it "raises IOError when file is not open in read mode" do + File.open(@fname, "w") do |file| + -> { file.pread(1, 1) }.should raise_error(IOError) + end + end + + it "raises IOError when file is closed" do + file = File.open(@fname, "r+") + file.close + -> { file.pread(1, 1) }.should raise_error(IOError) + end + end +end diff --git a/spec/ruby/core/io/print_spec.rb b/spec/ruby/core/io/print_spec.rb new file mode 100644 index 0000000000..085852024c --- /dev/null +++ b/spec/ruby/core/io/print_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#print" do + before :each do + @old_record_separator = $\ + @old_field_separator = $, + suppress_warning { + $\ = '->' + $, = '^^' + } + @name = tmp("io_print") + end + + after :each do + suppress_warning { + $\ = @old_record_separator + $, = @old_field_separator + } + rm_r @name + end + + it "returns nil" do + touch(@name) { |f| f.print.should be_nil } + end + + it "writes $_.to_s followed by $\\ (if any) to the stream if no arguments given" do + o = mock('o') + o.should_receive(:to_s).and_return("mockmockmock") + $_ = o + + touch(@name) { |f| f.print } + IO.read(@name).should == "mockmockmock#{$\}" + + # Set $_ to something known + string = File.open(__FILE__) {|f| f.gets } + + touch(@name) { |f| f.print } + IO.read(@name).should == "#{string}#{$\}" + end + + it "calls obj.to_s and not obj.to_str then writes the record separator" do + o = mock('o') + o.should_not_receive(:to_str) + o.should_receive(:to_s).and_return("hello") + + touch(@name) { |f| f.print(o) } + + IO.read(@name).should == "hello#{$\}" + end + + it "writes each obj.to_s to the stream separated by $, (if any) and appends $\\ (if any) given multiple objects" do + o, o2 = Object.new, Object.new + def o.to_s(); 'o'; end + def o2.to_s(); 'o2'; end + + suppress_warning { + touch(@name) { |f| f.print(o, o2) } + } + IO.read(@name).should == "#{o.to_s}#{$,}#{o2.to_s}#{$\}" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.print("stuff") }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/printf_spec.rb b/spec/ruby/core/io/printf_spec.rb new file mode 100644 index 0000000000..baa00f14ce --- /dev/null +++ b/spec/ruby/core/io/printf_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#printf" do + before :each do + @name = tmp("io_printf.txt") + @io = new_io @name + @io.sync = true + end + + after :each do + @io.close if @io + rm_r @name + end + + it "calls #to_str to convert the format object to a String" do + obj = mock("printf format") + obj.should_receive(:to_str).and_return("%s") + + @io.printf obj, "printf" + File.read(@name).should == "printf" + end + + it "writes the #sprintf formatted string" do + @io.printf "%d %s", 5, "cookies" + File.read(@name).should == "5 cookies" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.printf("stuff") }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/putc_spec.rb b/spec/ruby/core/io/putc_spec.rb new file mode 100644 index 0000000000..73473ad821 --- /dev/null +++ b/spec/ruby/core/io/putc_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/io/putc' + +describe "IO#putc" do + before :each do + @name = tmp("io_putc.txt") + @io_object = @io = new_io(@name) + end + + it_behaves_like :io_putc, :putc +end diff --git a/spec/ruby/core/io/puts_spec.rb b/spec/ruby/core/io/puts_spec.rb new file mode 100644 index 0000000000..a186ddaa5d --- /dev/null +++ b/spec/ruby/core/io/puts_spec.rb @@ -0,0 +1,139 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#puts" do + before :each do + @before_separator = $/ + @name = tmp("io_puts.txt") + @io = new_io @name + ScratchPad.record(+"") + def @io.write(str) + ScratchPad << str + end + end + + after :each do + ScratchPad.clear + @io.close if @io + rm_r @name + suppress_warning {$/ = @before_separator} + end + + it "writes just a newline when given no args" do + @io.puts.should == nil + ScratchPad.recorded.should == "\n" + end + + it "writes just a newline when given just a newline" do + -> { $stdout.puts "\n" }.should output_to_fd("\n", $stdout) + end + + it "writes empty string with a newline when given nil as an arg" do + @io.puts(nil).should == nil + ScratchPad.recorded.should == "\n" + end + + it "writes empty string with a newline when given nil as multiple args" do + @io.puts(nil, nil).should == nil + ScratchPad.recorded.should == "\n\n" + end + + it "calls :to_ary before writing non-string objects, regardless of it being implemented in the receiver" do + object = mock('hola') + object.should_receive(:method_missing).with(:to_ary) + object.should_receive(:to_s).and_return("#<Object:0x...>") + + @io.puts(object).should == nil + ScratchPad.recorded.should == "#<Object:0x...>\n" + end + + it "calls :to_ary before writing non-string objects" do + object = mock('hola') + object.should_receive(:to_ary).and_return(["hola"]) + + @io.puts(object).should == nil + ScratchPad.recorded.should == "hola\n" + end + + it "calls :to_s before writing non-string objects that don't respond to :to_ary" do + object = mock('hola') + object.should_receive(:to_s).and_return("hola") + + @io.puts(object).should == nil + ScratchPad.recorded.should == "hola\n" + end + + it "returns general object info if :to_s does not return a string" do + object = mock('hola') + object.should_receive(:to_s).and_return(false) + + @io.puts(object).should == nil + ScratchPad.recorded.should == object.inspect.split(" ")[0] + ">\n" + end + + it "writes each arg if given several" do + @io.puts(1, "two", 3).should == nil + ScratchPad.recorded.should == "1\ntwo\n3\n" + end + + it "flattens a nested array before writing it" do + @io.puts([1, 2, [3]]).should == nil + ScratchPad.recorded.should == "1\n2\n3\n" + end + + it "writes nothing for an empty array" do + x = [] + @io.should_not_receive(:write) + @io.puts(x).should == nil + end + + it "writes [...] for a recursive array arg" do + x = [] + x << 2 << x + @io.puts(x).should == nil + ScratchPad.recorded.should == "2\n[...]\n" + end + + it "writes a newline after objects that do not end in newlines" do + @io.puts(5).should == nil + ScratchPad.recorded.should == "5\n" + end + + it "does not write a newline after objects that end in newlines" do + @io.puts("5\n").should == nil + ScratchPad.recorded.should == "5\n" + end + + it "ignores the $/ separator global" do + suppress_warning {$/ = ":"} + @io.puts(5).should == nil + ScratchPad.recorded.should == "5\n" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.puts("stuff") }.should raise_error(IOError) + end + + it "writes crlf when IO is opened with newline: :crlf" do + File.open(@name, 'wt', newline: :crlf) do |file| + file.puts + end + File.binread(@name).should == "\r\n" + end + + it "writes cr when IO is opened with newline: :cr" do + File.open(@name, 'wt', newline: :cr) do |file| + file.puts + end + File.binread(@name).should == "\r" + end + + platform_is_not :windows do # https://bugs.ruby-lang.org/issues/12436 + it "writes lf when IO is opened with newline: :lf" do + File.open(@name, 'wt', newline: :lf) do |file| + file.puts + end + File.binread(@name).should == "\n" + end + end +end diff --git a/spec/ruby/core/io/pwrite_spec.rb b/spec/ruby/core/io/pwrite_spec.rb new file mode 100644 index 0000000000..2bc508b37d --- /dev/null +++ b/spec/ruby/core/io/pwrite_spec.rb @@ -0,0 +1,69 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +guard -> { platform_is_not :windows or ruby_version_is "3.3" } do + describe "IO#pwrite" do + before :each do + @fname = tmp("io_pwrite.txt") + @file = File.open(@fname, "w+") + end + + after :each do + @file.close + rm_r @fname + end + + it "returns the number of bytes written" do + @file.pwrite("foo", 0).should == 3 + end + + it "accepts a string and an offset" do + @file.pwrite("foo", 2) + @file.pread(3, 2).should == "foo" + end + + it "does not advance the pointer in the file" do + @file.pwrite("bar", 3) + @file.write("foo") + @file.pread(6, 0).should == "foobar" + end + + it "calls #to_s on the object to be written" do + object = mock("to_s") + object.should_receive(:to_s).and_return("foo") + @file.pwrite(object, 0) + @file.pread(3, 0).should == "foo" + end + + it "calls #to_int on the offset" do + offset = mock("to_int") + offset.should_receive(:to_int).and_return(2) + @file.pwrite("foo", offset) + @file.pread(3, 2).should == "foo" + end + + it "raises IOError when file is not open in write mode" do + File.open(@fname, "r") do |file| + -> { file.pwrite("foo", 1) }.should raise_error(IOError, "not opened for writing") + end + end + + it "raises IOError when file is closed" do + file = File.open(@fname, "w+") + file.close + -> { file.pwrite("foo", 1) }.should raise_error(IOError, "closed stream") + end + + it "raises a NoMethodError if object does not respond to #to_s" do + -> { + @file.pwrite(BasicObject.new, 0) + }.should raise_error(NoMethodError, /undefined method [`']to_s'/) + end + + it "raises a TypeError if the offset cannot be converted to an Integer" do + -> { + @file.pwrite("foo", Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + end +end diff --git a/spec/ruby/core/io/read_nonblock_spec.rb b/spec/ruby/core/io/read_nonblock_spec.rb new file mode 100644 index 0000000000..51e7cd6bd2 --- /dev/null +++ b/spec/ruby/core/io/read_nonblock_spec.rb @@ -0,0 +1,148 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#read_nonblock" do + before :each do + @read, @write = IO.pipe + end + + after :each do + @read.close if @read && !@read.closed? + @write.close if @write && !@write.closed? + end + + it "raises an exception extending IO::WaitReadable when there is no data" do + -> { @read.read_nonblock(5) }.should raise_error(IO::WaitReadable) { |e| + platform_is_not :windows do + e.should be_kind_of(Errno::EAGAIN) + end + platform_is :windows do + e.should be_kind_of(Errno::EWOULDBLOCK) + end + } + end + + context "when exception option is set to false" do + context "when there is no data" do + it "returns :wait_readable" do + @read.read_nonblock(5, exception: false).should == :wait_readable + end + end + + context "when the end is reached" do + it "returns nil" do + @write << "hello" + @write.close + + @read.read_nonblock(5) + + @read.read_nonblock(5, exception: false).should be_nil + end + end + end + + platform_is_not :windows do + it 'sets the IO in nonblock mode' do + require 'io/nonblock' + @write.write "abc" + @read.read_nonblock(1).should == "a" + @read.should.nonblock? + end + end + + it "returns at most the number of bytes requested" do + @write << "hello" + @read.read_nonblock(4).should == "hell" + end + + it "reads after ungetc with data in the buffer" do + @write.write("foobar") + @read.set_encoding( + 'utf-8', universal_newline: false + ) + c = @read.getc + @read.ungetc(c) + @read.read_nonblock(3).should == "foo" + @read.read_nonblock(3).should == "bar" + end + + it "raises an exception after ungetc with data in the buffer and character conversion enabled" do + @write.write("foobar") + @read.set_encoding( + 'utf-8', universal_newline: true + ) + c = @read.getc + @read.ungetc(c) + -> { @read.read_nonblock(3).should == "foo" }.should raise_error(IOError) + end + + it "returns less data if that is all that is available" do + @write << "hello" + @read.read_nonblock(10).should == "hello" + end + + it "allows for reading 0 bytes before any write" do + @read.read_nonblock(0).should == "" + end + + it "allows for reading 0 bytes after a write" do + @write.write "1" + @read.read_nonblock(0).should == "" + @read.read_nonblock(1).should == "1" + end + + it "raises ArgumentError when length is less than 0" do + -> { @read.read_nonblock(-1) }.should raise_error(ArgumentError) + end + + it "reads into the passed buffer" do + buffer = +"" + @write.write("1") + @read.read_nonblock(1, buffer) + buffer.should == "1" + end + + it "returns the passed buffer" do + buffer = +"" + @write.write("1") + output = @read.read_nonblock(1, buffer) + output.should equal(buffer) + end + + it "discards the existing buffer content upon successful read" do + buffer = +"existing content" + @write.write("hello world") + @write.close + @read.read_nonblock(11, buffer) + buffer.should == "hello world" + end + + it "discards the existing buffer content upon error" do + buffer = +"existing content" + @write.close + -> { @read.read_nonblock(1, buffer) }.should raise_error(EOFError) + buffer.should be_empty + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.read_nonblock(5) }.should raise_error(IOError) + end + + it "raises EOFError when the end is reached" do + @write << "hello" + @write.close + + @read.read_nonblock(5) + + -> { @read.read_nonblock(5) }.should raise_error(EOFError) + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @write.write("abc") + @write.close + @read.read_nonblock(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end +end diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb new file mode 100644 index 0000000000..988ec2ce30 --- /dev/null +++ b/spec/ruby/core/io/read_spec.rb @@ -0,0 +1,757 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.read" do + before :each do + @fname = tmp("io_read.txt") + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + end + + after :each do + rm_r @fname + end + + it "reads the contents of a file" do + IO.read(@fname).should == @contents + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(@fname) + IO.read(p) + end + + # https://bugs.ruby-lang.org/issues/19354 + it "accepts options as keyword arguments" do + IO.read(@fname, 3, 0, mode: "r+").should == @contents[0, 3] + + -> { + IO.read(@fname, 3, 0, {mode: "r+"}) + }.should raise_error(ArgumentError, /wrong number of arguments/) + end + + it "accepts an empty options Hash" do + IO.read(@fname, **{}).should == @contents + end + + it "accepts a length, and empty options Hash" do + IO.read(@fname, 3, **{}).should == @contents[0, 3] + end + + it "accepts a length, offset, and empty options Hash" do + IO.read(@fname, 3, 0, **{}).should == @contents[0, 3] + end + + it "raises an IOError if the options Hash specifies write mode" do + -> { IO.read(@fname, 3, 0, mode: "w") }.should raise_error(IOError) + end + + it "raises an IOError if the options Hash specifies append only mode" do + -> { IO.read(@fname, mode: "a") }.should raise_error(IOError) + end + + it "reads the file if the options Hash includes read mode" do + IO.read(@fname, mode: "r").should == @contents + end + + it "reads the file if the options Hash includes read/write mode" do + IO.read(@fname, mode: "r+").should == @contents + end + + it "reads the file if the options Hash includes read/write append mode" do + IO.read(@fname, mode: "a+").should == @contents + end + + platform_is_not :windows do + ruby_version_is ""..."3.3" do + it "uses an :open_args option" do + string = IO.read(@fname, nil, 0, open_args: ["r", nil, {encoding: Encoding::US_ASCII}]) + string.encoding.should == Encoding::US_ASCII + + string = IO.read(@fname, nil, 0, open_args: ["r", nil, {}]) + string.encoding.should == Encoding::UTF_8 + end + end + end + + it "disregards other options if :open_args is given" do + string = IO.read(@fname,mode: "w", encoding: Encoding::UTF_32LE, open_args: ["r", encoding: Encoding::UTF_8]) + string.encoding.should == Encoding::UTF_8 + end + + it "doesn't require mode to be specified in :open_args" do + string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII}]) + string.encoding.should == Encoding::US_ASCII + end + + it "doesn't require mode to be specified in :open_args even if flags option passed" do + string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII, flags: File::CREAT}]) + string.encoding.should == Encoding::US_ASCII + end + + it "treats second nil argument as no length limit" do + IO.read(@fname, nil).should == @contents + IO.read(@fname, nil, 5).should == IO.read(@fname, @contents.length, 5) + end + + it "treats third nil argument as 0" do + IO.read(@fname, nil, nil).should == @contents + IO.read(@fname, 5, nil).should == IO.read(@fname, 5, 0) + end + + it "reads the contents of a file up to a certain size when specified" do + IO.read(@fname, 5).should == @contents.slice(0..4) + end + + it "reads the contents of a file from an offset of a specific size when specified" do + IO.read(@fname, 5, 3).should == @contents.slice(3, 5) + end + + it "returns nil at end-of-file when length is passed" do + IO.read(@fname, 1, 10).should == nil + end + + it "returns an empty string when reading zero bytes" do + IO.read(@fname, 0).should == '' + end + + it "returns a String in BINARY when passed a size" do + IO.read(@fname, 1).encoding.should == Encoding::BINARY + IO.read(@fname, 0).encoding.should == Encoding::BINARY + end + + it "raises an Errno::ENOENT when the requested file does not exist" do + rm_r @fname + -> { IO.read @fname }.should raise_error(Errno::ENOENT) + end + + it "raises a TypeError when not passed a String type" do + -> { IO.read nil }.should raise_error(TypeError) + end + + it "raises an ArgumentError when not passed a valid length" do + -> { IO.read @fname, -1 }.should raise_error(ArgumentError) + end + + ruby_version_is ''...'3.3' do + it "raises an Errno::EINVAL when not passed a valid offset" do + -> { IO.read @fname, 0, -1 }.should raise_error(Errno::EINVAL) + -> { IO.read @fname, -1, -1 }.should raise_error(Errno::EINVAL) + end + end + + ruby_version_is '3.3' do + it "raises an ArgumentError when not passed a valid offset" do + -> { IO.read @fname, 0, -1 }.should raise_error(ArgumentError) + -> { IO.read @fname, -1, -1 }.should raise_error(ArgumentError) + end + end + + it "uses the external encoding specified via the :external_encoding option" do + str = IO.read(@fname, external_encoding: Encoding::ISO_8859_1) + str.encoding.should == Encoding::ISO_8859_1 + end + + it "uses the external encoding specified via the :encoding option" do + str = IO.read(@fname, encoding: Encoding::ISO_8859_1) + str.encoding.should == Encoding::ISO_8859_1 + end + + platform_is :windows do + it "reads the file in text mode" do + # 0x1A is CTRL+Z and is EOF in Windows text mode. + File.binwrite(@fname, "\x1Abbb") + IO.read(@fname).should.empty? + end + end +end + +ruby_version_is ""..."4.0" do + describe "IO.read from a pipe" do + it "runs the rest as a subprocess and returns the standard output" do + cmd = "|sh -c 'echo hello'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello" + end + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read(cmd).should == "hello\n" + end + end + + platform_is_not :windows do + it "opens a pipe to a fork if the rest is -" do + str = nil + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + str = IO.read("|-") + end + + if str # parent + str.should == "hello from child\n" + else #child + puts "hello from child" + exit! + end + end + end + + it "reads only the specified number of bytes requested" do + cmd = "|sh -c 'echo hello'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello" + end + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read(cmd, 1).should == "h" + end + end + + platform_is_not :windows do + it "raises Errno::ESPIPE if passed an offset" do + -> { + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read("|sh -c 'echo hello'", 1, 1) + end + }.should raise_error(Errno::ESPIPE) + end + end + + quarantine! do # The process tried to write to a nonexistent pipe. + platform_is :windows do + # TODO: It should raise Errno::ESPIPE on Windows as well + # once https://bugs.ruby-lang.org/issues/12230 is fixed. + it "raises Errno::EINVAL if passed an offset" do + -> { + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read("|cmd.exe /C echo hello", 1, 1) + end + }.should raise_error(Errno::EINVAL) + end + end + end + + ruby_version_is "3.3" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation" do + cmd = "|echo ok" + -> { + IO.read(cmd) + }.should complain(/IO process creation with a leading '\|'/) + end + end + end +end + +describe "IO.read on an empty file" do + before :each do + @fname = tmp("io_read_empty.txt") + touch(@fname) + end + + after :each do + rm_r @fname + end + + it "returns nil when length is passed" do + IO.read(@fname, 1).should == nil + end + + it "returns an empty string when no length is passed" do + IO.read(@fname).should == "" + end +end + +describe "IO#read" do + + before :each do + @fname = tmp("io_read.txt") + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + + @io = open @fname, "r+" + end + + after :each do + @io.close + rm_r @fname + end + + it "can be read from consecutively" do + @io.read(1).should == '1' + @io.read(2).should == '23' + @io.read(3).should == '456' + @io.read(4).should == '7890' + end + + it "treats first nil argument as no length limit" do + @io.read(nil).should == @contents + end + + it "raises an ArgumentError when not passed a valid length" do + -> { @io.read(-1) }.should raise_error(ArgumentError) + end + + it "clears the output buffer if there is nothing to read" do + @io.pos = 10 + + buf = +'non-empty string' + + @io.read(10, buf).should == nil + + buf.should == '' + + buf = +'non-empty string' + + @io.read(nil, buf).should == "" + + buf.should == '' + + buf = +'non-empty string' + + @io.read(0, buf).should == "" + + buf.should == '' + end + + it "raise FrozenError if the output buffer is frozen" do + @io.read + -> { @io.read(0, 'frozen-string'.freeze) }.should raise_error(FrozenError) + -> { @io.read(1, 'frozen-string'.freeze) }.should raise_error(FrozenError) + -> { @io.read(nil, 'frozen-string'.freeze) }.should raise_error(FrozenError) + end + + ruby_bug "", ""..."3.3" do + it "raise FrozenError if the output buffer is frozen (2)" do + @io.read + -> { @io.read(1, ''.freeze) }.should raise_error(FrozenError) + end + end + + it "consumes zero bytes when reading zero bytes" do + @io.read(0).should == '' + @io.pos.should == 0 + + @io.getc.should == '1' + end + + it "is at end-of-file when everything has been read" do + @io.read + @io.should.eof? + end + + it "reads the contents of a file" do + @io.read.should == @contents + end + + it "places the specified number of bytes in the buffer" do + buf = +"" + @io.read 5, buf + + buf.should == "12345" + end + + it "expands the buffer when too small" do + buf = +"ABCDE" + @io.read nil, buf + + buf.should == @contents + end + + it "overwrites the buffer" do + buf = +"ABCDEFGHIJ" + @io.read nil, buf + + buf.should == @contents + end + + it "truncates the buffer when too big" do + buf = +"ABCDEFGHIJKLMNO" + @io.read nil, buf + buf.should == @contents + + @io.rewind + + buf = +"ABCDEFGHIJKLMNO" + @io.read 5, buf + buf.should == @contents[0..4] + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @io.read(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end + + # https://bugs.ruby-lang.org/issues/20416 + it "does not preserve the encoding of the given buffer when max length is not provided" do + buffer = ''.encode(Encoding::ISO_8859_1) + @io.read(nil, buffer) + + buffer.encoding.should_not == Encoding::ISO_8859_1 + end + + it "returns the given buffer" do + buf = +"" + + @io.read(nil, buf).should equal buf + end + + it "returns the given buffer when there is nothing to read" do + buf = +"" + + @io.read + @io.read(nil, buf).should equal buf + end + + it "coerces the second argument to string and uses it as a buffer" do + buf = +"ABCDE" + obj = mock("buff") + obj.should_receive(:to_str).any_number_of_times.and_return(buf) + + @io.read(15, obj).should_not equal obj + buf.should == @contents + end + + it "returns an empty string at end-of-file" do + @io.read + @io.read.should == '' + end + + it "reads the contents of a file when more bytes are specified" do + @io.read(@contents.length + 1).should == @contents + end + + it "returns an empty string at end-of-file" do + @io.read + @io.read.should == '' + end + + it "returns an empty string when the current pos is bigger than the content size" do + @io.pos = 1000 + @io.read.should == '' + end + + it "returns nil at end-of-file with a length" do + @io.read + @io.read(1).should == nil + end + + it "with length argument returns nil when the current pos is bigger than the content size" do + @io.pos = 1000 + @io.read(1).should == nil + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.read }.should raise_error(IOError) + end + + it "raises ArgumentError when length is less than 0" do + -> { @io.read(-1) }.should raise_error(ArgumentError) + end + + platform_is_not :windows do + it "raises IOError when stream is closed by another thread" do + r, w = IO.pipe + t = Thread.new do + begin + r.read(1) + rescue => e + e + end + end + + Thread.pass until t.stop? + r.close + t.join + t.value.should be_kind_of(IOError) + w.close + end + end +end + +platform_is :windows do + describe "IO#read on Windows" do + before :each do + @fname = tmp("io_read.txt") + touch(@fname, "wb") { |f| f.write "a\r\nb\r\nc" } + end + + after :each do + @io.close if @io + rm_r @fname + end + + it "normalizes line endings in text mode" do + @io = new_io(@fname, "r") + @io.read.should == "a\nb\nc" + end + + it "does not normalize line endings in binary mode" do + @io = new_io(@fname, "rb") + @io.read.should == "a\r\nb\r\nc" + end + end +end + +describe "IO#read" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "ignores unicode encoding" do + @io.readline.should == "Voici la ligne une.\n" + # read "Qui è" + @io.read(5).should == "Qui " + [195].pack('C*') + end +end + +describe "IO#read in binary mode" do + before :each do + @internal = Encoding.default_internal + @name = fixture __FILE__, "read_binary.txt" + end + + after :each do + Encoding.default_internal = @internal + end + + it "does not transcode file contents when Encoding.default_internal is set" do + Encoding.default_internal = "utf-8" + + result = File.open(@name, "rb") { |f| f.read }.chomp + + result.encoding.should == Encoding::BINARY + xE2 = [226].pack('C*') + result.should == ("abc" + xE2 + "def").force_encoding(Encoding::BINARY) + end +end + +describe "IO#read in text mode" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + @name = fixture __FILE__, "read_text.txt" + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "reads data according to the internal encoding" do + Encoding.default_internal = "utf-8" + Encoding.default_external = "utf-8" + + result = File.open(@name, "rt") { |f| f.read }.chomp + + result.encoding.should == Encoding::UTF_8 + result.should == "abcâdef" + end +end + +describe "IO.read with BOM" do + it "reads a file without a bom" do + name = fixture __FILE__, "no_bom_UTF-8.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "UTF-8\n" + end + + it "reads a file with a utf-8 bom" do + name = fixture __FILE__, "bom_UTF-8.txt" + result = File.read(name, mode: "rb:BOM|utf-16le") + result.force_encoding("binary").should == "UTF-8\n" + end + + it "reads a file with a utf-16le bom" do + name = fixture __FILE__, "bom_UTF-16LE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "U\x00T\x00F\x00-\x001\x006\x00L\x00E\x00\n\x00" + end + + it "reads a file with a utf-16be bom" do + name = fixture __FILE__, "bom_UTF-16BE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "\x00U\x00T\x00F\x00-\x001\x006\x00B\x00E\x00\n" + end + + it "reads a file with a utf-32le bom" do + name = fixture __FILE__, "bom_UTF-32LE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "U\x00\x00\x00T\x00\x00\x00F\x00\x00\x00-\x00\x00\x003\x00\x00\x002\x00\x00\x00L\x00\x00\x00E\x00\x00\x00\n\x00\x00\x00" + end + + it "reads a file with a utf-32be bom" do + name = fixture __FILE__, "bom_UTF-32BE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "\x00\x00\x00U\x00\x00\x00T\x00\x00\x00F\x00\x00\x00-\x00\x00\x003\x00\x00\x002\x00\x00\x00B\x00\x00\x00E\x00\x00\x00\n" + end +end + +describe :io_read_internal_encoding, shared: true do + it "returns a transcoded String" do + @io.read.should == "ありがとう\n" + end + + it "sets the String encoding to the internal encoding" do + @io.read.encoding.should equal(Encoding::UTF_8) + end + + describe "when passed nil for limit" do + it "sets the buffer to a transcoded String" do + result = @io.read(nil, buf = +"") + buf.should equal(result) + buf.should == "ありがとう\n" + end + + it "sets the buffer's encoding to the internal encoding" do + buf = "".dup.force_encoding Encoding::ISO_8859_1 + @io.read(nil, buf) + buf.encoding.should equal(Encoding::UTF_8) + end + end +end + +describe :io_read_size_internal_encoding, shared: true do + it "reads bytes when passed a size" do + @io.read(2).should == [164, 162].pack('C*').force_encoding(Encoding::BINARY) + end + + it "returns a String in BINARY when passed a size" do + @io.read(4).encoding.should equal(Encoding::BINARY) + @io.read(0).encoding.should equal(Encoding::BINARY) + end + + it "does not change the buffer's encoding when passed a limit" do + buf = "".dup.force_encoding Encoding::ISO_8859_1 + @io.read(4, buf) + buf.should == [164, 162, 164, 234].pack('C*').force_encoding(Encoding::ISO_8859_1) + buf.encoding.should equal(Encoding::ISO_8859_1) + end + + it "truncates the buffer but does not change the buffer's encoding when no data remains" do + buf = "abc".dup.force_encoding Encoding::ISO_8859_1 + @io.read + + @io.read(1, buf).should be_nil + buf.size.should == 0 + buf.encoding.should equal(Encoding::ISO_8859_1) + end +end + +describe "IO#read" do + describe "when IO#external_encoding and IO#internal_encoding are nil" do + before :each do + @name = tmp("io_read.txt") + touch(@name) { |f| f.write "\x00\x01\x02" } + @io = new_io @name, "r+" + end + + after :each do + @io.close if @io + rm_r @name + end + + it "sets the String encoding to Encoding.default_external" do + @io.read.encoding.should equal(Encoding.default_external) + end + end + + describe "with internal encoding" do + after :each do + @io.close if @io + end + + describe "not specified" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp" + end + + it "does not transcode the String" do + @io.read.should == ("ありがとう\n").encode(Encoding::EUC_JP) + end + + it "sets the String encoding to the external encoding" do + @io.read.encoding.should equal(Encoding::EUC_JP) + end + + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by open mode" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8" + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by mode: option" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8" + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by internal_encoding: option" do + before :each do + options = { mode: "r", + internal_encoding: "utf-8", + external_encoding: "euc-jp" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by encoding: option" do + before :each do + options = { mode: "r", encoding: "euc-jp:utf-8" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + end +end + +describe "IO#read with large data" do + before :each do + # TODO: what is the significance of this mystery math? + @data_size = 8096 * 2 + 1024 + @data = "*" * @data_size + + @fname = tmp("io_read.txt") + touch(@fname) { |f| f.write @data } + end + + after :each do + rm_r @fname + end + + it "reads all the data at once" do + File.open(@fname, 'r') { |io| ScratchPad.record io.read } + + ScratchPad.recorded.size.should == @data_size + ScratchPad.recorded.should == @data + end + + it "reads only the requested number of bytes" do + read_size = @data_size / 2 + File.open(@fname, 'r') { |io| ScratchPad.record io.read(read_size) } + + ScratchPad.recorded.size.should == read_size + ScratchPad.recorded.should == @data[0, read_size] + end +end diff --git a/spec/ruby/core/io/readbyte_spec.rb b/spec/ruby/core/io/readbyte_spec.rb new file mode 100644 index 0000000000..14426c28ac --- /dev/null +++ b/spec/ruby/core/io/readbyte_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "IO#readbyte" do + before :each do + @io = File.open(__FILE__, 'r') + end + + after :each do + @io.close + end + + it "reads one byte from the stream" do + byte = @io.readbyte + byte.should == ?r.getbyte(0) + @io.pos.should == 1 + end + + it "raises EOFError on EOF" do + @io.seek(999999) + -> do + @io.readbyte + end.should raise_error EOFError + end +end diff --git a/spec/ruby/core/io/readchar_spec.rb b/spec/ruby/core/io/readchar_spec.rb new file mode 100644 index 0000000000..a66773851a --- /dev/null +++ b/spec/ruby/core/io/readchar_spec.rb @@ -0,0 +1,110 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_readchar_internal_encoding, shared: true do + it "returns a transcoded String" do + @io.readchar.should == "あ" + end + + it "sets the String encoding to the internal encoding" do + @io.readchar.encoding.should equal(Encoding::UTF_8) + end +end + +describe "IO#readchar" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns the next string from the stream" do + @io.readchar.should == 'V' + @io.readchar.should == 'o' + @io.readchar.should == 'i' + # read the rest of line + @io.readline.should == "ci la ligne une.\n" + @io.readchar.should == 'Q' + end + + it "raises an EOFError when invoked at the end of the stream" do + @io.read + -> { @io.readchar }.should raise_error(EOFError) + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.readchar }.should raise_error(IOError) + end +end + +describe "IO#readchar with internal encoding" do + after :each do + @io.close if @io + end + + describe "not specified" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp" + end + + it "does not transcode the String" do + @io.readchar.should == ("あ").encode(Encoding::EUC_JP) + end + + it "sets the String encoding to the external encoding" do + @io.readchar.encoding.should equal(Encoding::EUC_JP) + end + end + + describe "specified by open mode" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8" + end + + it_behaves_like :io_readchar_internal_encoding, nil + end + + describe "specified by mode: option" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8" + end + + it_behaves_like :io_readchar_internal_encoding, nil + end + + describe "specified by internal_encoding: option" do + before :each do + options = { mode: "r", + internal_encoding: "utf-8", + external_encoding: "euc-jp" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_readchar_internal_encoding, nil + end + + describe "specified by encoding: option" do + before :each do + options = { mode: "r", encoding: "euc-jp:utf-8" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_readchar_internal_encoding, nil + end +end + +describe "IO#readchar" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "raises EOFError on empty stream" do + -> { @io.readchar }.should raise_error(EOFError) + end +end diff --git a/spec/ruby/core/io/readline_spec.rb b/spec/ruby/core/io/readline_spec.rb new file mode 100644 index 0000000000..a814c1be90 --- /dev/null +++ b/spec/ruby/core/io/readline_spec.rb @@ -0,0 +1,84 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#readline" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns the next line on the stream" do + @io.readline.should == "Voici la ligne une.\n" + @io.readline.should == "Qui è la linea due.\n" + end + + it "goes back to first position after a rewind" do + @io.readline.should == "Voici la ligne une.\n" + @io.rewind + @io.readline.should == "Voici la ligne une.\n" + end + + it "returns characters after the position set by #seek" do + @io.seek(1) + @io.readline.should == "oici la ligne une.\n" + end + + it "raises EOFError on end of stream" do + IOSpecs.lines.length.times { @io.readline } + -> { @io.readline }.should raise_error(EOFError) + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.readline }.should raise_error(IOError) + end + + it "assigns the returned line to $_" do + IOSpecs.lines.each do |line| + @io.readline + $_.should == line + end + end + + describe "when passed limit" do + it "reads limit bytes" do + @io.readline(3).should == "Voi" + end + + it "returns an empty string when passed 0 as a limit" do + @io.readline(0).should == "" + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { @io.readline(2**128) }.should raise_error(RangeError) + end + end + + describe "when passed separator and limit" do + it "reads limit bytes till the separator" do + # Voici la ligne une.\ + @io.readline(" ", 4).should == "Voic" + @io.readline(" ", 4).should == "i " + @io.readline(" ", 4).should == "la " + @io.readline(" ", 4).should == "lign" + @io.readline(" ", 4).should == "e " + end + end + + describe "when passed chomp" do + it "returns the first line without a trailing newline character" do + @io.readline(chomp: true).should == IOSpecs.lines_without_newline_characters[0] + end + + it "raises exception when options passed as Hash" do + -> { @io.readline({ chomp: true }) }.should raise_error(TypeError) + + -> { + @io.readline("\n", 1, { chomp: true }) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end +end diff --git a/spec/ruby/core/io/readlines_spec.rb b/spec/ruby/core/io/readlines_spec.rb new file mode 100644 index 0000000000..b4770775d1 --- /dev/null +++ b/spec/ruby/core/io/readlines_spec.rb @@ -0,0 +1,259 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/readlines' + +describe "IO#readlines" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @orig_extenc = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + end + + after :each do + @io.close unless @io.closed? + Encoding.default_external = @orig_extenc + end + + it "raises an IOError if the stream is closed" do + @io.close + -> { @io.readlines }.should raise_error(IOError) + end + + describe "when passed no arguments" do + before :each do + suppress_warning {@sep, $/ = $/, " "} + end + + after :each do + suppress_warning {$/ = @sep} + end + + it "returns an Array containing lines based on $/" do + @io.readlines.should == IOSpecs.lines_space_separator + end + end + + describe "when passed no arguments" do + it "updates self's position" do + @io.readlines + @io.pos.should eql(137) + end + + it "updates self's lineno based on the number of lines read" do + @io.readlines + @io.lineno.should eql(9) + end + + it "does not change $_" do + $_ = "test" + @io.readlines + $_.should == "test" + end + + it "returns an empty Array when self is at the end" do + @io.readlines.should == IOSpecs.lines + @io.readlines.should == [] + end + end + + describe "when passed nil" do + it "returns the remaining content as one line starting at the current position" do + @io.readlines(nil).should == [IOSpecs.lines.join] + end + end + + describe "when passed an empty String" do + it "returns an Array containing all paragraphs" do + @io.readlines("").should == IOSpecs.paragraphs + end + end + + describe "when passed a separator" do + it "returns an Array containing lines based on the separator" do + @io.readlines("r").should == IOSpecs.lines_r_separator + end + + it "returns an empty Array when self is at the end" do + @io.readlines + @io.readlines("r").should == [] + end + + it "updates self's lineno based on the number of lines read" do + @io.readlines("r") + @io.lineno.should eql(5) + end + + it "updates self's position based on the number of characters read" do + @io.readlines("r") + @io.pos.should eql(137) + end + + it "does not change $_" do + $_ = "test" + @io.readlines("r") + $_.should == "test" + end + + it "tries to convert the passed separator to a String using #to_str" do + obj = mock('to_str') + obj.stub!(:to_str).and_return("r") + @io.readlines(obj).should == IOSpecs.lines_r_separator + end + end + + describe "when passed limit" do + it "raises ArgumentError when passed 0 as a limit" do + -> { @io.readlines(0) }.should raise_error(ArgumentError) + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { @io.readlines(2**128) }.should raise_error(RangeError) + end + end + + describe "when passed chomp" do + it "returns the first line without a trailing newline character" do + @io.readlines(chomp: true).should == IOSpecs.lines_without_newline_characters + end + + it "raises exception when options passed as Hash" do + -> { @io.readlines({ chomp: true }) }.should raise_error(TypeError) + + -> { + @io.readlines("\n", 1, { chomp: true }) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end + + describe "when passed arbitrary keyword argument" do + it "tolerates it" do + @io.readlines(chomp: true, foo: :bar).should == IOSpecs.lines_without_newline_characters + end + end +end + +describe "IO#readlines" do + before :each do + @name = tmp("io_readlines") + end + + after :each do + rm_r @name + end + + it "raises an IOError if the stream is opened for append only" do + -> do + File.open(@name, "a:utf-8") { |f| f.readlines } + end.should raise_error(IOError) + end + + it "raises an IOError if the stream is opened for write only" do + -> do + File.open(@name, "w:utf-8") { |f| f.readlines } + end.should raise_error(IOError) + end +end + +describe "IO.readlines" do + before :each do + @external = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + + @name = fixture __FILE__, "lines.txt" + ScratchPad.record [] + end + + after :each do + Encoding.default_external = @external + end + + it "does not change $_" do + $_ = "test" + IO.readlines(@name) + $_.should == "test" + end + + ruby_version_is ""..."4.0" do + describe "when passed a string that starts with a |" do + it "gets data from the standard out of the subprocess" do + cmd = "|sh -c 'echo hello;echo line2'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello&echo line2" + end + + lines = nil + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + lines = IO.readlines(cmd) + end + lines.should == ["hello\n", "line2\n"] + end + + platform_is_not :windows do + it "gets data from a fork when passed -" do + lines = nil + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + lines = IO.readlines("|-") + end + + if lines # parent + lines.should == ["hello\n", "from a fork\n"] + else + puts "hello" + puts "from a fork" + exit! + end + end + end + end + + ruby_version_is "3.3" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + cmd = "|echo ok" + -> { + IO.readlines(cmd) + }.should complain(/IO process creation with a leading '\|'/) + end + end + end + + it_behaves_like :io_readlines, :readlines + it_behaves_like :io_readlines_options_19, :readlines +end + +describe "IO.readlines" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + @name = fixture __FILE__, "lines.txt" + @dollar_slash = $/ + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + suppress_warning {$/ = @dollar_slash} + end + + it "encodes lines using the default external encoding" do + Encoding.default_external = Encoding::UTF_8 + lines = IO.readlines(@name) + lines.all? { |s| s.encoding == Encoding::UTF_8 }.should be_true + end + + it "encodes lines using the default internal encoding, when set" do + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = Encoding::UTF_16 + suppress_warning {$/ = $/.encode Encoding::UTF_16} + lines = IO.readlines(@name) + lines.all? { |s| s.encoding == Encoding::UTF_16 }.should be_true + end + + it "ignores the default internal encoding if the external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + lines = IO.readlines(@name) + lines.all? { |s| s.encoding == Encoding::BINARY }.should be_true + end +end diff --git a/spec/ruby/core/io/readpartial_spec.rb b/spec/ruby/core/io/readpartial_spec.rb new file mode 100644 index 0000000000..176c33cf9e --- /dev/null +++ b/spec/ruby/core/io/readpartial_spec.rb @@ -0,0 +1,115 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#readpartial" do + before :each do + @rd, @wr = IO.pipe + @rd.binmode + @wr.binmode + end + + after :each do + @rd.close unless @rd.closed? + @wr.close unless @wr.closed? + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.readpartial(10) }.should raise_error(IOError) + + @rd.close + -> { @rd.readpartial(10) }.should raise_error(IOError) + end + + it "reads at most the specified number of bytes" do + @wr.write("foobar") + + # buffered read + @rd.read(1).should == 'f' + # return only specified number, not the whole buffer + @rd.readpartial(1).should == "o" + end + + it "reads after ungetc with data in the buffer" do + @wr.write("foobar") + c = @rd.getc + @rd.ungetc(c) + @rd.readpartial(3).should == "foo" + @rd.readpartial(3).should == "bar" + end + + it "reads after ungetc with multibyte characters in the buffer" do + @wr.write("∂φ/∂x = gaîté") + c = @rd.getc + @rd.ungetc(c) + @rd.readpartial(3).should == "\xE2\x88\x82" + @rd.readpartial(3).should == "\xCF\x86/" + end + + it "reads after ungetc without data in the buffer" do + @wr.write("f") + c = @rd.getc + @rd.ungetc(c) + @rd.readpartial(2).should == "f" + + # now, also check that the ungot char is cleared and + # not returned again + @wr.write("b") + @rd.readpartial(2).should == "b" + end + + it "discards the existing buffer content upon successful read" do + buffer = +"existing content" + @wr.write("hello world") + @wr.close + @rd.readpartial(11, buffer).should.equal?(buffer) + buffer.should == "hello world" + end + + it "raises EOFError on EOF" do + @wr.write("abc") + @wr.close + @rd.readpartial(10).should == 'abc' + -> { @rd.readpartial(10) }.should raise_error(EOFError) + end + + it "discards the existing buffer content upon error" do + buffer = +'hello' + @wr.close + -> { @rd.readpartial(1, buffer) }.should raise_error(EOFError) + buffer.should be_empty + end + + it "raises IOError if the stream is closed" do + @wr.close + -> { @rd.readpartial(1) }.should raise_error(IOError) + end + + it "raises ArgumentError if the negative argument is provided" do + -> { @rd.readpartial(-1) }.should raise_error(ArgumentError) + end + + it "immediately returns an empty string if the length argument is 0" do + @rd.readpartial(0).should == "" + end + + it "raises IOError if the stream is closed and the length argument is 0" do + @rd.close + -> { @rd.readpartial(0) }.should raise_error(IOError, "closed stream") + end + + it "clears and returns the given buffer if the length argument is 0" do + buffer = +"existing content" + @rd.readpartial(0, buffer).should == buffer + buffer.should == "" + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @wr.write("abc") + @wr.close + @rd.readpartial(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end +end diff --git a/spec/ruby/core/io/reopen_spec.rb b/spec/ruby/core/io/reopen_spec.rb new file mode 100644 index 0000000000..8ff0f217f4 --- /dev/null +++ b/spec/ruby/core/io/reopen_spec.rb @@ -0,0 +1,313 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +require 'fcntl' + +describe "IO#reopen" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + + @io = new_io @name + @other_io = File.open @other_name, "w" + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "calls #to_io to convert an object" do + obj = mock("io") + obj.should_receive(:to_io).and_return(@other_io) + @io.reopen obj + end + + it "changes the class of the instance to the class of the object returned by #to_io" do + obj = mock("io") + obj.should_receive(:to_io).and_return(@other_io) + @io.reopen(obj).should be_an_instance_of(File) + end + + it "raises an IOError if the object returned by #to_io is closed" do + obj = mock("io") + obj.should_receive(:to_io).and_return(IOSpecs.closed_io) + -> { @io.reopen obj }.should raise_error(IOError) + end + + it "raises a TypeError if #to_io does not return an IO instance" do + obj = mock("io") + obj.should_receive(:to_io).and_return("something else") + -> { @io.reopen obj }.should raise_error(TypeError) + end + + 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_error(IOError) + end + + it "raises an IOError if the IO argument is closed" do + -> { @io.reopen(IOSpecs.closed_io) }.should raise_error(IOError) + end + + it "raises an IOError when called on a closed stream with an IO" do + @io.close + -> { @io.reopen(STDOUT) }.should raise_error(IOError) + end +end + +describe "IO#reopen with a String" do + before :each do + @name = fixture __FILE__, "numbered_lines.txt" + @other_name = tmp("io_reopen.txt") + touch @other_name + @io = IOSpecs.io_fixture "lines.txt" + + @tmp_file = tmp("reopen") + end + + after :each do + @io.close unless @io.closed? + rm_r @other_name, @tmp_file + end + + it "does not raise an exception when called on a closed stream with a path" do + @io.close + @io.reopen @name, "r" + @io.closed?.should be_false + @io.gets.should == "Line 1: One\n" + end + + it "returns self" do + @io.reopen(@name).should equal(@io) + end + + it "positions a newly created instance at the beginning of the new stream" do + @io.reopen(@name) + @io.gets.should == "Line 1: One\n" + end + + it "positions an instance that has been read from at the beginning of the new stream" do + @io.gets + @io.reopen(@name) + @io.gets.should == "Line 1: One\n" + end + + platform_is_not :windows do + it "passes all mode flags through" do + @io.reopen(@tmp_file, "ab") + (@io.fcntl(Fcntl::F_GETFL) & File::APPEND).should == File::APPEND + end + end + + platform_is_not :windows do + # TODO Should this work on Windows? + it "affects exec/system/fork performed after it" do + ruby_exe fixture(__FILE__, "reopen_stdout.rb"), args: @tmp_file + File.read(@tmp_file).should == "from system\nfrom exec\n" + end + end + + it "calls #to_path on non-String arguments" do + obj = mock('path') + obj.should_receive(:to_path).and_return(@other_name) + @io.reopen(obj) + end +end + +describe "IO#reopen with a String" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + @other_io = nil + + rm_r @other_name + end + + after :each do + @io.close unless @io.closed? + @other_io.close if @other_io and not @other_io.closed? + rm_r @name, @other_name + end + + it "opens a path after writing to the original file descriptor" do + @io = new_io @name, "w" + + @io.print "original data" + @io.reopen @other_name + @io.print "new data" + @io.flush + + File.read(@name).should == "original data" + File.read(@other_name).should == "new data" + end + + it "always resets the close-on-exec flag to true on non-STDIO objects" do + @io = new_io @name, "w" + + @io.close_on_exec = true + @io.reopen @other_name + @io.should.close_on_exec? + + @io.close_on_exec = false + @io.reopen @other_name + @io.should.close_on_exec? + end + + it "creates the file if it doesn't exist if the IO is opened in write mode" do + @io = new_io @name, "w" + + @io.reopen(@other_name) + File.should.exist?(@other_name) + end + + it "creates the file if it doesn't exist if the IO is opened in write mode" do + @io = new_io @name, "a" + + @io.reopen(@other_name) + File.should.exist?(@other_name) + end +end + +describe "IO#reopen with a String" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + + touch @name + rm_r @other_name + end + + after :each do + @io.close + rm_r @name, @other_name + end + + it "raises an Errno::ENOENT if the file does not exist and the IO is not opened in write mode" do + @io = new_io @name, "r" + -> { @io.reopen(@other_name) }.should raise_error(Errno::ENOENT) + end +end + +describe "IO#reopen with an IO at EOF" do + before :each do + @name = tmp("io_reopen.txt") + touch(@name) { |f| f.puts "a line" } + @other_name = tmp("io_reopen_other.txt") + touch(@other_name) do |f| + f.puts "Line 1" + f.puts "Line 2" + end + + @io = new_io @name, "r" + @other_io = new_io @other_name, "r" + @io.read + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "resets the EOF status to false" do + @io.eof?.should be_true + @io.reopen @other_io + @io.eof?.should be_false + end +end + +describe "IO#reopen with an IO" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + touch(@other_name) do |f| + f.puts "Line 1" + f.puts "Line 2" + end + + @io = new_io @name + @other_io = IO.new(new_fd(@other_name, "r"), "r") + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "does not call #to_io" do + # Why do we not use #should_not_receive(:to_io) here? Because + # MRI actually changes the class of @io in the call to #reopen + # but does not preserve the existing singleton class of @io. + def @io.to_io; flunk; end + @io.reopen(@other_io).should be_an_instance_of(IO) + end + + it "does not change the object_id" do + obj_id = @io.object_id + @io.reopen @other_io + @io.object_id.should == obj_id + end + + it "reads from the beginning if the other IO has not been read from" do + @io.reopen @other_io + @io.gets.should == "Line 1\n" + end + + it "reads from the current position of the other IO's stream" do + @other_io.gets.should == "Line 1\n" + @io.reopen @other_io + @io.gets.should == "Line 2\n" + end +end + +describe "IO#reopen with an IO" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + + @io = new_io @name + @other_io = File.open @other_name, "w" + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "associates the IO instance with the other IO's stream" do + File.read(@other_name).should == "" + @io.reopen @other_io + @io.print "io data" + @io.flush + File.read(@name).should == "" + File.read(@other_name).should == "io data" + end + + it "always resets the close-on-exec flag to true on non-STDIO objects" do + @other_io.close_on_exec = true + @io.close_on_exec = true + @io.reopen @other_io + @io.should.close_on_exec? + + @other_io.close_on_exec = false + @io.close_on_exec = false + @io.reopen @other_io + @io.should.close_on_exec? + end + + it "may change the class of the instance" do + @io.reopen @other_io + @io.should be_an_instance_of(File) + end + + it "sets path equals to the other IO's path if other IO is File" do + @io.reopen @other_io + @io.path.should == @other_io.path + end +end diff --git a/spec/ruby/core/io/rewind_spec.rb b/spec/ruby/core/io/rewind_spec.rb new file mode 100644 index 0000000000..5579cbd988 --- /dev/null +++ b/spec/ruby/core/io/rewind_spec.rb @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#rewind" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "positions the instance to the beginning of input" do + @io.readline.should == "Voici la ligne une.\n" + @io.readline.should == "Qui è la linea due.\n" + @io.rewind + @io.readline.should == "Voici la ligne une.\n" + end + + it "positions the instance to the beginning of output for write-only IO" do + name = tmp("io_rewind_spec") + io = File.open(name, "w") + io.write("Voici la ligne une.\n") + io.rewind + io.pos.should == 0 + ensure + io.close + rm_r name + end + + it "positions the instance to the beginning of input and clears EOF" do + value = @io.read + @io.rewind + @io.should_not.eof? + value.should == @io.read + end + + it "sets lineno to 0" do + @io.readline.should == "Voici la ligne une.\n" + @io.lineno.should == 1 + @io.rewind + @io.lineno.should == 0 + end + + it "returns 0" do + @io.rewind.should == 0 + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.rewind }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/seek_spec.rb b/spec/ruby/core/io/seek_spec.rb new file mode 100644 index 0000000000..2fa4a73ac9 --- /dev/null +++ b/spec/ruby/core/io/seek_spec.rb @@ -0,0 +1,79 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#seek" do + it_behaves_like :io_set_pos, :seek +end + +describe "IO#seek" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "moves the read position relative to the current position with SEEK_CUR" do + -> { @io.seek(-1) }.should raise_error(Errno::EINVAL) + @io.seek(10, IO::SEEK_CUR) + @io.readline.should == "igne une.\n" + @io.seek(-5, IO::SEEK_CUR) + @io.readline.should == "une.\n" + end + + it "moves the read position relative to the start with SEEK_SET" do + @io.seek(1) + @io.pos.should == 1 + @io.rewind + @io.seek(43, IO::SEEK_SET) + @io.readline.should == "Aquí está la línea tres.\n" + @io.seek(5, IO::SEEK_SET) + @io.readline.should == " la ligne une.\n" + end + + it "moves the read position relative to the end with SEEK_END" do + @io.seek(0, IO::SEEK_END) + @io.tell.should == 137 + @io.seek(-25, IO::SEEK_END) + @io.readline.should == "cinco.\n" + end + + it "moves the read position and clears EOF with SEEK_SET" do + value = @io.read + @io.seek(0, IO::SEEK_SET) + @io.should_not.eof? + value.should == @io.read + end + + it "moves the read position and clears EOF with SEEK_CUR" do + value = @io.read + @io.seek(-1, IO::SEEK_CUR) + @io.should_not.eof? + value[-1].should == @io.read[0] + end + + it "moves the read position and clears EOF with SEEK_END" do + value = @io.read + @io.seek(-1, IO::SEEK_END) + @io.should_not.eof? + value[-1].should == @io.read[0] + end + + platform_is :darwin do + it "supports seek offsets greater than 2^32" do + begin + zero = File.open('/dev/zero') + offset = 2**33 + zero.seek(offset, File::SEEK_SET) + pos = zero.pos + + pos.should == offset + ensure + zero.close rescue nil + end + end + end +end diff --git a/spec/ruby/core/io/select_spec.rb b/spec/ruby/core/io/select_spec.rb new file mode 100644 index 0000000000..3893e7620f --- /dev/null +++ b/spec/ruby/core/io/select_spec.rb @@ -0,0 +1,164 @@ +require_relative '../../spec_helper' + +describe "IO.select" do + before :each do + @rd, @wr = IO.pipe + end + + after :each do + @rd.close unless @rd.closed? + @wr.close unless @wr.closed? + end + + it "blocks for duration of timeout and returns nil if there are no objects ready for I/O" do + IO.select([@rd], nil, nil, 0.001).should == nil + end + + it "returns immediately all objects that are ready for I/O when timeout is 0" do + @wr.syswrite("be ready") + IO.pipe do |_, wr| + result = IO.select [@rd], [wr], nil, 0 + result.should == [[@rd], [wr], []] + end + end + + it "returns nil after timeout if there are no objects ready for I/O" do + result = IO.select [@rd], nil, nil, 0 + result.should == nil + end + + it "returns supplied objects when they are ready for I/O" do + main = Thread.current + t = Thread.new { + Thread.pass until main.status == "sleep" + @wr.write "be ready" + } + result = IO.select [@rd], nil, nil, nil + result.should == [[@rd], [], []] + t.join + end + + it "leaves out IO objects for which there is no I/O ready" do + @wr.write "be ready" + platform_is :aix do + # In AIX, when a pipe is readable, select(2) returns the write side + # of the pipe as "readable", even though you cannot actually read + # anything from the write side. + result = IO.select [@wr, @rd], nil, nil, nil + result.should == [[@wr, @rd], [], []] + end + platform_is_not :aix do + # Order matters here. We want to see that @wr doesn't expand the size + # of the returned array, so it must be 1st. + result = IO.select [@wr, @rd], nil, nil, nil + result.should == [[@rd], [], []] + end + end + + it "returns supplied objects correctly when monitoring the same object in different arrays" do + filename = tmp("IO_select_pipe_file") + io = File.open(filename, 'w+') + result = IO.select [io], [io], nil, 0 + result.should == [[io], [io], []] + io.close + rm_r filename + end + + it "returns the pipe read end in read set if the pipe write end is closed concurrently" do + main = Thread.current + t = Thread.new { + Thread.pass until main.stop? + @wr.close + } + IO.select([@rd]).should == [[@rd], [], []] + ensure + t.join + end + + it "invokes to_io on supplied objects that are not IO and returns the supplied objects" do + # make some data available + @wr.write("foobar") + + obj = mock("read_io") + obj.should_receive(:to_io).at_least(1).and_return(@rd) + IO.select([obj]).should == [[obj], [], []] + + IO.pipe do |_, wr| + obj = mock("write_io") + obj.should_receive(:to_io).at_least(1).and_return(wr) + IO.select(nil, [obj]).should == [[], [obj], []] + end + end + + it "raises TypeError if supplied objects are not IO" do + -> { IO.select([Object.new]) }.should raise_error(TypeError) + -> { IO.select(nil, [Object.new]) }.should raise_error(TypeError) + + obj = mock("io") + obj.should_receive(:to_io).any_number_of_times.and_return(nil) + + -> { IO.select([obj]) }.should raise_error(TypeError) + -> { IO.select(nil, [obj]) }.should raise_error(TypeError) + end + + it "raises a TypeError if the specified timeout value is not Numeric" do + -> { IO.select([@rd], nil, nil, Object.new) }.should raise_error(TypeError) + end + + it "raises TypeError if the first three arguments are not Arrays" do + -> { IO.select(Object.new)}.should raise_error(TypeError) + -> { IO.select(nil, Object.new)}.should raise_error(TypeError) + -> { IO.select(nil, nil, Object.new)}.should raise_error(TypeError) + end + + it "raises an ArgumentError when passed a negative timeout" do + -> { IO.select(nil, nil, nil, -5)}.should raise_error(ArgumentError) + end + + describe "returns the available descriptors when the file descriptor" do + it "is in both read and error arrays" do + @wr.write("foobar") + result = IO.select([@rd], nil, [@rd]) + result.should == [[@rd], [], []] + end + + it "is in both write and error arrays" do + result = IO.select(nil, [@wr], [@wr]) + result.should == [[], [@wr], []] + end + + it "is in both read and write arrays" do + filename = tmp("IO_select_read_write_file") + w = File.open(filename, 'w+') + begin + IO.select([w], [w], []).should == [[w], [w], []] + ensure + w.close + rm_r filename + end + + IO.select([@wr], [@wr], []).should == [[], [@wr], []] + + @wr.write("foobar") + # CRuby on macOS returns [[@rd], [@rd], []], weird but we accept it here, probably only for pipe read-end + [ + [[@rd], [], []], + [[@rd], [@rd], []] + ].should.include? IO.select([@rd], [@rd], []) + end + end +end + +describe "IO.select when passed nil for timeout" do + it "sleeps forever and sets the thread status to 'sleep'" do + t = Thread.new do + IO.select(nil, nil, nil, nil) + end + + Thread.pass while t.status && t.status != "sleep" + t.join unless t.status + t.status.should == "sleep" + t.kill + t.join + end +end diff --git a/spec/ruby/core/io/set_encoding_by_bom_spec.rb b/spec/ruby/core/io/set_encoding_by_bom_spec.rb new file mode 100644 index 0000000000..30d5ce5a5a --- /dev/null +++ b/spec/ruby/core/io/set_encoding_by_bom_spec.rb @@ -0,0 +1,262 @@ +require_relative '../../spec_helper' + +describe "IO#set_encoding_by_bom" do + before :each do + @name = tmp('io_set_encoding_by_bom.txt') + touch(@name) + @io = new_io(@name, 'rb') + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "returns nil if not readable" do + not_readable_io = new_io(@name, 'wb') + + not_readable_io.set_encoding_by_bom.should be_nil + not_readable_io.external_encoding.should == Encoding::ASCII_8BIT + ensure + not_readable_io.close + end + + it "returns the result encoding if found BOM UTF-8 sequence" do + File.binwrite(@name, "\u{FEFF}") + + @io.set_encoding_by_bom.should == Encoding::UTF_8 + @io.external_encoding.should == Encoding::UTF_8 + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\u{FEFF}abc") + + @io.set_encoding_by_bom.should == Encoding::UTF_8 + @io.external_encoding.should == Encoding::UTF_8 + @io.read.b.should == "abc".b + end + + it "returns the result encoding if found BOM UTF_16LE sequence" do + File.binwrite(@name, "\xFF\xFE") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\xFF\xFEabc") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "abc".b + end + + it "returns the result encoding if found BOM UTF_16BE sequence" do + File.binwrite(@name, "\xFE\xFF") + + @io.set_encoding_by_bom.should == Encoding::UTF_16BE + @io.external_encoding.should == Encoding::UTF_16BE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\xFE\xFFabcd") + + @io.set_encoding_by_bom.should == Encoding::UTF_16BE + @io.external_encoding.should == Encoding::UTF_16BE + @io.read.b.should == "abcd".b + end + + it "returns the result encoding if found BOM UTF_32LE sequence" do + File.binwrite(@name, "\xFF\xFE\x00\x00") + + @io.set_encoding_by_bom.should == Encoding::UTF_32LE + @io.external_encoding.should == Encoding::UTF_32LE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\xFF\xFE\x00\x00abc") + + @io.set_encoding_by_bom.should == Encoding::UTF_32LE + @io.external_encoding.should == Encoding::UTF_32LE + @io.read.b.should == "abc".b + end + + it "returns the result encoding if found BOM UTF_32BE sequence" do + File.binwrite(@name, "\x00\x00\xFE\xFF") + + @io.set_encoding_by_bom.should == Encoding::UTF_32BE + @io.external_encoding.should == Encoding::UTF_32BE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\x00\x00\xFE\xFFabcd") + + @io.set_encoding_by_bom.should == Encoding::UTF_32BE + @io.external_encoding.should == Encoding::UTF_32BE + @io.read.b.should == "abcd".b + end + + it "returns nil if io is empty" do + @io.set_encoding_by_bom.should be_nil + @io.external_encoding.should == Encoding::ASCII_8BIT + end + + it "returns nil if UTF-8 BOM sequence is incomplete" do + File.write(@name, "\xEF") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEF".b + @io.rewind + + File.write(@name, "\xEFa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEFa".b + @io.rewind + + File.write(@name, "\xEF\xBB") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEF\xBB".b + @io.rewind + + File.write(@name, "\xEF\xBBa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEF\xBBa".b + end + + it "returns nil if UTF-16BE BOM sequence is incomplete" do + File.write(@name, "\xFE") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFE".b + @io.rewind + + File.write(@name, "\xFEa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFEa".b + end + + it "returns nil if UTF-16LE/UTF-32LE BOM sequence is incomplete" do + File.write(@name, "\xFF") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFF".b + @io.rewind + + File.write(@name, "\xFFa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFFa".b + end + + it "returns UTF-16LE if UTF-32LE BOM sequence is incomplete" do + File.write(@name, "\xFF\xFE") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.write(@name, "\xFF\xFE\x00") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "\x00".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.write(@name, "\xFF\xFE\x00a") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "\x00a".b + end + + it "returns nil if UTF-32BE BOM sequence is incomplete" do + File.write(@name, "\x00") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00".b + @io.rewind + + File.write(@name, "\x00a") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00a".b + @io.rewind + + File.write(@name, "\x00\x00") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00".b + @io.rewind + + File.write(@name, "\x00\x00a") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00a".b + @io.rewind + + File.write(@name, "\x00\x00\xFE") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00\xFE".b + @io.rewind + + File.write(@name, "\x00\x00\xFEa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00\xFEa".b + end + + it "returns nil if found BOM sequence not provided" do + File.write(@name, "abc") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read(3).should == "abc".b + end + + it 'returns exception if io not in binary mode' do + not_binary_io = new_io(@name, 'r') + + -> { not_binary_io.set_encoding_by_bom }.should raise_error(ArgumentError, 'ASCII incompatible encoding needs binmode') + ensure + not_binary_io.close + end + + it 'returns exception if encoding already set' do + @io.set_encoding("utf-8") + + -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding is set to UTF-8 already') + end + + it 'returns exception if encoding conversion is already set' do + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE) + + -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding conversion is set') + end +end diff --git a/spec/ruby/core/io/set_encoding_spec.rb b/spec/ruby/core/io/set_encoding_spec.rb new file mode 100644 index 0000000000..22d9017635 --- /dev/null +++ b/spec/ruby/core/io/set_encoding_spec.rb @@ -0,0 +1,238 @@ +require_relative '../../spec_helper' + +describe :io_set_encoding_write, shared: true do + it "sets the encodings to nil when they were set previously" do + @io = new_io @name, "#{@object}:ibm437:ibm866" + @io.set_encoding nil, nil + + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + end + + it "sets the encodings to nil when the IO is built with no explicit encoding" do + @io = new_io @name, @object + + # Checking our assumptions first + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + + @io.set_encoding nil, nil + + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + end + + it "prevents the encodings from changing when Encoding defaults are changed" do + @io = new_io @name, "#{@object}:utf-8:us-ascii" + @io.set_encoding nil, nil + + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + end + + it "sets the encodings to the current Encoding defaults" do + @io = new_io @name, @object + + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.set_encoding nil, nil + + @io.external_encoding.should == Encoding::IBM437 + @io.internal_encoding.should == Encoding::IBM866 + end +end + +describe "IO#set_encoding when passed nil, nil" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + # The defaults + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil + + @name = tmp('io_set_encoding.txt') + touch(@name) + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + + @io.close if @io and not @io.closed? + rm_r @name + end + + describe "with 'r' mode" do + it "sets the encodings to the current Encoding defaults" do + @io = new_io @name, "r" + + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.set_encoding nil, nil + @io.external_encoding.should equal(Encoding::IBM437) + @io.internal_encoding.should equal(Encoding::IBM866) + end + + it "prevents the #internal_encoding from changing when Encoding.default_internal is changed" do + @io = new_io @name, "r" + @io.set_encoding nil, nil + + Encoding.default_internal = Encoding::IBM437 + + @io.internal_encoding.should be_nil + end + + it "allows the #external_encoding to change when Encoding.default_external is changed" do + @io = new_io @name, "r" + @io.set_encoding nil, nil + + Encoding.default_external = Encoding::IBM437 + + @io.external_encoding.should equal(Encoding::IBM437) + end + end + + describe "with 'rb' mode" do + it "returns Encoding.default_external" do + @io = new_io @name, "rb" + @io.external_encoding.should equal(Encoding::BINARY) + + @io.set_encoding nil, nil + @io.external_encoding.should equal(Encoding.default_external) + end + end + + describe "with 'r+' mode" do + it_behaves_like :io_set_encoding_write, nil, "r+" + end + + describe "with 'w' mode" do + it_behaves_like :io_set_encoding_write, nil, "w" + end + + describe "with 'w+' mode" do + it_behaves_like :io_set_encoding_write, nil, "w+" + end + + describe "with 'a' mode" do + it_behaves_like :io_set_encoding_write, nil, "a" + end + + describe "with 'a+' mode" do + it_behaves_like :io_set_encoding_write, nil, "a+" + end + + describe "with standard IOs" do + it "correctly resets them" do + STDOUT.external_encoding.should == nil + STDOUT.internal_encoding.should == nil + + begin + STDOUT.set_encoding(Encoding::US_ASCII, Encoding::ISO_8859_1) + ensure + STDOUT.set_encoding(nil, nil) + end + + STDOUT.external_encoding.should == nil + STDOUT.internal_encoding.should == nil + end + end +end + +describe "IO#set_encoding" do + before :each do + @name = tmp('io_set_encoding.txt') + touch(@name) + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "returns self" do + @io.set_encoding(Encoding::UTF_8).should equal(@io) + end + + it "sets the external encoding when passed an Encoding argument" do + @io.set_encoding(Encoding::UTF_8) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "sets the external and internal encoding when passed two Encoding arguments" do + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "sets the external encoding when passed the name of an Encoding" do + @io.set_encoding("utf-8") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "ignores the internal encoding if the same as external when passed Encoding objects" do + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_8) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "ignores the internal encoding if the same as external when passed encoding names separated by ':'" do + @io.set_encoding("utf-8:utf-8") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "sets the external and internal encoding when passed the names of Encodings separated by ':'" do + @io.set_encoding("utf-8:utf-16be") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "sets the external and internal encoding when passed two String arguments" do + @io.set_encoding("utf-8", "utf-16be") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "calls #to_str to convert an abject to a String" do + obj = mock("io_set_encoding") + obj.should_receive(:to_str).and_return("utf-8:utf-16be") + @io.set_encoding(obj) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "calls #to_str to convert the second argument to a String" do + obj = mock("io_set_encoding") + obj.should_receive(:to_str).at_least(1).times.and_return("utf-16be") + @io.set_encoding(Encoding::UTF_8, obj) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "saves encoding options passed as a hash in the last argument" do + File.write(@name, "\xff") + io = File.open(@name) + io.set_encoding(Encoding::EUC_JP, Encoding::SHIFT_JIS, invalid: :replace, replace: ".") + io.read.should == "." + ensure + io.close + end + + it "raises ArgumentError when no arguments are given" do + -> { @io.set_encoding() }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when too many arguments are given" do + -> { @io.set_encoding(1, 2, 3) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/shared/binwrite.rb b/spec/ruby/core/io/shared/binwrite.rb new file mode 100644 index 0000000000..e51093329b --- /dev/null +++ b/spec/ruby/core/io/shared/binwrite.rb @@ -0,0 +1,91 @@ +require_relative '../fixtures/classes' + +describe :io_binwrite, shared: true do + before :each do + @filename = tmp("IO_binwrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file << "012345678901234567890123456789" + end + end + + after :each do + rm_r @filename + end + + it "coerces the argument to a string using to_s" do + (obj = mock('test')).should_receive(:to_s).and_return('a string') + IO.send(@method, @filename, obj) + end + + it "returns the number of bytes written" do + IO.send(@method, @filename, "abcde").should == 5 + end + + it "accepts options as a keyword argument" do + IO.send(@method, @filename, "hi", 0, flags: File::CREAT).should == 2 + + -> { + IO.send(@method, @filename, "hi", 0, {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 2..3)") + end + + it "creates a file if missing" do + fn = @filename + "xxx" + begin + File.should_not.exist?(fn) + IO.send(@method, fn, "test") + File.should.exist?(fn) + ensure + rm_r fn + end + end + + it "creates file if missing even if offset given" do + fn = @filename + "xxx" + begin + File.should_not.exist?(fn) + IO.send(@method, fn, "test", 0) + File.should.exist?(fn) + ensure + rm_r fn + end + end + + it "truncates the file and writes the given string" do + IO.send(@method, @filename, "hello, world!") + File.read(@filename).should == "hello, world!" + end + + it "doesn't truncate the file and writes the given string if an offset is given" do + IO.send(@method, @filename, "hello, world!", 0) + File.read(@filename).should == "hello, world!34567890123456789" + IO.send(@method, @filename, "hello, world!", 20) + File.read(@filename).should == "hello, world!3456789hello, world!" + end + + it "doesn't truncate and writes at the given offset after passing empty opts" do + IO.send(@method, @filename, "hello world!", 1, **{}) + File.read(@filename).should == "0hello world!34567890123456789" + end + + it "accepts a :mode option" do + IO.send(@method, @filename, "hello, world!", mode: 'a') + File.read(@filename).should == "012345678901234567890123456789hello, world!" + IO.send(@method, @filename, "foo", 2, mode: 'w') + File.read(@filename).should == "\0\0foo" + end + + it "accepts a :flags option without :mode one" do + IO.send(@method, @filename, "hello, world!", flags: File::CREAT) + File.read(@filename).should == "hello, world!" + end + + it "raises an error if readonly mode is specified" do + -> { IO.send(@method, @filename, "abcde", mode: "r") }.should raise_error(IOError) + end + + it "truncates if empty :opts provided and offset skipped" do + IO.send(@method, @filename, "hello, world!", **{}) + File.read(@filename).should == "hello, world!" + end +end diff --git a/spec/ruby/core/io/shared/chars.rb b/spec/ruby/core/io/shared/chars.rb new file mode 100644 index 0000000000..266566f221 --- /dev/null +++ b/spec/ruby/core/io/shared/chars.rb @@ -0,0 +1,73 @@ +# -*- encoding: utf-8 -*- +describe :io_chars, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + end + + after :each do + @io.close unless @io.closed? + end + + it "yields each character" do + @io.readline.should == "Voici la ligne une.\n" + + count = 0 + @io.send(@method) do |c| + ScratchPad << c + break if 4 < count += 1 + end + + ScratchPad.recorded.should == ["Q", "u", "i", " ", "è"] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.send(@method) + enum.should be_an_instance_of(Enumerator) + enum.first(5).should == ["V", "o", "i", "c", "i"] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.send(@method).size.should == nil + end + end + end + end + + it "returns itself" do + @io.send(@method) { |c| }.should equal(@io) + end + + it "returns an enumerator for a closed stream" do + IOSpecs.closed_io.send(@method).should be_an_instance_of(Enumerator) + end + + it "raises an IOError when an enumerator created on a closed stream is accessed" do + -> { IOSpecs.closed_io.send(@method).first }.should raise_error(IOError) + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send(@method) {} }.should raise_error(IOError) + end +end + +describe :io_chars_empty, shared: true do + before :each do + @name = tmp("io_each_char") + @io = new_io @name, "w+:utf-8" + ScratchPad.record [] + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "does not yield any characters on an empty stream" do + @io.send(@method) { |c| ScratchPad << c } + ScratchPad.recorded.should == [] + end +end diff --git a/spec/ruby/core/io/shared/codepoints.rb b/spec/ruby/core/io/shared/codepoints.rb new file mode 100644 index 0000000000..6872846c1a --- /dev/null +++ b/spec/ruby/core/io/shared/codepoints.rb @@ -0,0 +1,54 @@ +# -*- encoding: utf-8 -*- +require_relative '../fixtures/classes' + +describe :io_codepoints, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @enum = @io.send(@method) + end + + after :each do + @io.close + end + + describe "when no block is given" do + it "returns an Enumerator" do + @enum.should be_an_instance_of(Enumerator) + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @enum.size.should == nil + end + end + end + end + + it "yields each codepoint" do + @enum.first(25).should == [ + 86, 111, 105, 99, 105, 32, 108, 97, 32, 108, 105, 103, 110, + 101, 32, 117, 110, 101, 46, 10, 81, 117, 105, 32, 232 + ] + end + + it "yields each codepoint starting from the current position" do + @io.pos = 130 + @enum.to_a.should == [101, 32, 115, 105, 120, 46, 10] + end + + it "raises an error if reading invalid sequence" do + @io.pos = 60 # inside of a multibyte sequence + -> { @enum.first }.should raise_error(ArgumentError) + end + + it "does not change $_" do + $_ = "test" + @enum.to_a + $_.should == "test" + end + + it "raises an IOError when self is not readable" do + -> { IOSpecs.closed_io.send(@method).to_a }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/shared/each.rb b/spec/ruby/core/io/shared/each.rb new file mode 100644 index 0000000000..0747f31b8a --- /dev/null +++ b/spec/ruby/core/io/shared/each.rb @@ -0,0 +1,251 @@ +# -*- encoding: utf-8 -*- +require_relative '../fixtures/classes' + +describe :io_each, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + end + + after :each do + @io.close if @io + end + + describe "with no separator" do + it "yields each line to the passed block" do + @io.send(@method) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines + end + + it "yields each line starting from the current position" do + @io.pos = 41 + @io.send(@method) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines[2..-1] + end + + it "returns self" do + @io.send(@method) { |l| l }.should equal(@io) + end + + it "does not change $_" do + $_ = "test" + @io.send(@method) { |s| s } + $_.should == "test" + end + + it "raises an IOError when self is not readable" do + -> { IOSpecs.closed_io.send(@method) {} }.should raise_error(IOError) + end + + it "makes line count accessible via lineno" do + @io.send(@method) { ScratchPad << @io.lineno } + ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] + end + + it "makes line count accessible via $." do + @io.send(@method) { ScratchPad << $. } + ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.send(@method) + enum.should be_an_instance_of(Enumerator) + + enum.each { |l| ScratchPad << l } + ScratchPad.recorded.should == IOSpecs.lines + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.send(@method).size.should == nil + end + end + end + end + end + + describe "with limit" do + describe "when limit is 0" do + it "raises an ArgumentError" do + # must pass block so Enumerator is evaluated and raises + -> { @io.send(@method, 0){} }.should raise_error(ArgumentError) + end + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { @io.send(@method, 2**128){} }.should raise_error(RangeError) + end + end + + describe "when passed a String containing one space as a separator" do + it "uses the passed argument as the line separator" do + @io.send(@method, " ") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end + + it "does not change $_" do + $_ = "test" + @io.send(@method, " ") { |s| } + $_.should == "test" + end + + it "tries to convert the passed separator to a String using #to_str" do + obj = mock("to_str") + obj.stub!(:to_str).and_return(" ") + + @io.send(@method, obj) { |l| ScratchPad << l } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end + end + + describe "when passed nil as a separator" do + it "yields self's content starting from the current position when the passed separator is nil" do + @io.pos = 100 + @io.send(@method, nil) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed an empty String as a separator" do + it "yields each paragraph" do + @io.send(@method, "") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs + end + + it "discards leading newlines" do + @io.readline + @io.readline + @io.send(@method, "") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] + end + end + + describe "with both separator and limit" do + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.send(@method, nil, 1024) + enum.should be_an_instance_of(Enumerator) + + enum.each { |l| ScratchPad << l } + ScratchPad.recorded.should == [IOSpecs.lines.join] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.send(@method, nil, 1024).size.should == nil + end + end + end + end + + describe "when a block is given" do + it "accepts an empty block" do + @io.send(@method, nil, 1024) {}.should equal(@io) + end + + describe "when passed nil as a separator" do + it "yields self's content starting from the current position when the passed separator is nil" do + @io.pos = 100 + @io.send(@method, nil, 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed an empty String as a separator" do + it "yields each paragraph" do + @io.send(@method, "", 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs + end + + it "discards leading newlines" do + @io.readline + @io.readline + @io.send(@method, "", 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] + end + end + end + end + + describe "when passed chomp" do + it "yields each line without trailing newline characters to the passed block" do + @io.send(@method, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_without_newline_characters + end + + it "raises exception when options passed as Hash" do + -> { + @io.send(@method, { chomp: true }) { |s| } + }.should raise_error(TypeError) + + -> { + @io.send(@method, "\n", 1, { chomp: true }) { |s| } + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end + + describe "when passed chomp and a separator" do + it "yields each line without separator to the passed block" do + @io.send(@method, " ", chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator_without_trailing_spaces + end + end + + describe "when passed chomp and empty line as a separator" do + it "yields each paragraph without trailing new line characters" do + @io.send(@method, "", 1024, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs_without_trailing_new_line_characters + end + end + + describe "when passed chomp and nil as a separator" do + it "yields self's content" do + @io.pos = 100 + @io.send(@method, nil, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed chomp, nil as a separator, and a limit" do + it "yields each line of limit size without truncating trailing new line character" do + # 43 - is a size of the 1st paragraph in the file + @io.send(@method, nil, 43, chomp: true) { |s| ScratchPad << s } + + ScratchPad.recorded.should == [ + "Voici la ligne une.\nQui è la linea due.\n\n\n", + "Aquí está la línea tres.\n" + "Hier ist Zeile ", + "vier.\n\nEstá aqui a linha cinco.\nHere is li", + "ne six.\n" + ] + end + end + + describe "when passed too many arguments" do + it "raises ArgumentError" do + -> { + @io.send(@method, "", 1, "excess argument", chomp: true) {} + }.should raise_error(ArgumentError) + end + end +end + +describe :io_each_default_separator, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + suppress_warning {@sep, $/ = $/, " "} + end + + after :each do + @io.close if @io + suppress_warning {$/ = @sep} + end + + it "uses $/ as the default line separator" do + @io.send(@method) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end +end diff --git a/spec/ruby/core/io/shared/gets_ascii.rb b/spec/ruby/core/io/shared/gets_ascii.rb new file mode 100644 index 0000000000..2bd5470d99 --- /dev/null +++ b/spec/ruby/core/io/shared/gets_ascii.rb @@ -0,0 +1,19 @@ +# encoding: binary +describe :io_gets_ascii, shared: true do + describe "with ASCII separator" do + before :each do + @name = tmp("gets_specs.txt") + touch(@name, "wb") { |f| f.print "this is a test\xFFtesty\ntestier" } + + File.open(@name, "rb") { |f| @data = f.send(@method, "\xFF") } + end + + after :each do + rm_r @name + end + + it "returns the separator's character representation" do + @data.should == "this is a test\xFF" + end + end +end diff --git a/spec/ruby/core/io/shared/new.rb b/spec/ruby/core/io/shared/new.rb new file mode 100644 index 0000000000..e84133493c --- /dev/null +++ b/spec/ruby/core/io/shared/new.rb @@ -0,0 +1,413 @@ +require_relative '../fixtures/classes' + +# NOTE: should be synchronized with library/stringio/initialize_spec.rb + +# This group of specs may ONLY contain specs that do successfully create +# an IO instance from the file descriptor returned by #new_fd helper. +describe :io_new, shared: true do + before :each do + @name = tmp("io_new.txt") + @fd = new_fd @name + @io = nil + end + + after :each do + if @io + @io.close + elsif @fd + IO.new(@fd, "w").close + end + rm_r @name + end + + it "creates an IO instance from an Integer argument" do + @io = IO.send(@method, @fd, "w") + @io.should be_an_instance_of(IO) + end + + it "creates an IO instance when STDOUT is closed" do + suppress_warning do + stdout = STDOUT + stdout_file = tmp("stdout.txt") + + begin + @io = IO.send(@method, @fd, "w") + @io.should be_an_instance_of(IO) + ensure + STDOUT = stdout + rm_r stdout_file + end + end + end + + it "creates an IO instance when STDERR is closed" do + suppress_warning do + stderr = STDERR + stderr_file = tmp("stderr.txt") + STDERR = new_io stderr_file + STDERR.close + + begin + @io = IO.send(@method, @fd, "w") + @io.should be_an_instance_of(IO) + ensure + STDERR = stderr + rm_r stderr_file + end + end + end + + it "calls #to_int on an object to convert to an Integer" do + obj = mock("file descriptor") + obj.should_receive(:to_int).and_return(@fd) + @io = IO.send(@method, obj, "w") + @io.should be_an_instance_of(IO) + end + + it "accepts options as keyword arguments" do + @io = IO.send(@method, @fd, "w", flags: File::CREAT) + @io.write("foo").should == 3 + + -> { + IO.send(@method, @fd, "w", {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 1..2)") + end + + it "accepts a :mode option" do + @io = IO.send(@method, @fd, mode: "w") + @io.write("foo").should == 3 + end + + it "accepts a mode argument set to nil with a valid :mode option" do + @io = IO.send(@method, @fd, nil, mode: "w") + @io.write("foo").should == 3 + end + + it "accepts a mode argument with a :mode option set to nil" do + @io = IO.send(@method, @fd, "w", mode: nil) + @io.write("foo").should == 3 + end + + it "uses the external encoding specified in the mode argument" do + @io = IO.send(@method, @fd, 'w:utf-8') + @io.external_encoding.to_s.should == 'UTF-8' + end + + it "uses the external and the internal encoding specified in the mode argument" do + @io = IO.send(@method, @fd, 'w:utf-8:ISO-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end + + it "uses the external encoding specified via the :external_encoding option" do + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8') + @io.external_encoding.to_s.should == 'UTF-8' + end + + it "uses the internal encoding specified via the :internal_encoding option" do + @io = IO.send(@method, @fd, 'w', internal_encoding: 'ibm866') + @io.internal_encoding.to_s.should == 'IBM866' + end + + it "uses the colon-separated encodings specified via the :encoding option" do + @io = IO.send(@method, @fd, 'w', encoding: 'utf-8:ISO-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end + + it "uses the :encoding option as the external encoding when only one is given" do + @io = IO.send(@method, @fd, 'w', encoding: 'ISO-8859-1') + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "uses the :encoding options as the external encoding when it's an Encoding object" do + @io = IO.send(@method, @fd, 'w', encoding: Encoding::ISO_8859_1) + @io.external_encoding.should == Encoding::ISO_8859_1 + end + + it "ignores the :encoding option when the :external_encoding option is present" do + -> { + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', encoding: 'iso-8859-1:iso-8859-1') + }.should complain(/Ignoring encoding parameter/) + @io.external_encoding.to_s.should == 'UTF-8' + end + + it "ignores the :encoding option when the :internal_encoding option is present" do + -> { + @io = IO.send(@method, @fd, 'w', internal_encoding: 'ibm866', encoding: 'iso-8859-1:iso-8859-1') + }.should complain(/Ignoring encoding parameter/) + @io.internal_encoding.to_s.should == 'IBM866' + end + + it "uses the encoding specified via the :mode option hash" do + @io = IO.send(@method, @fd, mode: 'w:utf-8:ISO-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end + + it "ignores the :internal_encoding option when the same as the external encoding" do + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', internal_encoding: 'utf-8') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == '' + end + + it "sets internal encoding to nil when passed '-'" do + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', internal_encoding: '-') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == '' + end + + it "sets binmode from mode string" do + @io = IO.send(@method, @fd, 'wb') + @io.should.binmode? + end + + it "does not set binmode without being asked" do + @io = IO.send(@method, @fd, 'w') + @io.should_not.binmode? + end + + it "sets binmode from :binmode option" do + @io = IO.send(@method, @fd, 'w', binmode: true) + @io.should.binmode? + end + + it "does not set binmode from false :binmode" do + @io = IO.send(@method, @fd, 'w', binmode: false) + @io.should_not.binmode? + end + + it "sets external encoding to binary with binmode in mode string" do + @io = IO.send(@method, @fd, 'wb') + @io.external_encoding.should == Encoding::BINARY + end + + # #5917 + it "sets external encoding to binary with :binmode option" do + @io = IO.send(@method, @fd, 'w', binmode: true) + @io.external_encoding.should == Encoding::BINARY + end + + it "does not use binary encoding when mode encoding is specified" do + @io = IO.send(@method, @fd, 'wb:iso-8859-1') + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use binary encoding when :encoding option is specified" do + @io = IO.send(@method, @fd, 'wb', encoding: "iso-8859-1") + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use binary encoding when :external_encoding option is specified" do + @io = IO.send(@method, @fd, 'wb', external_encoding: "iso-8859-1") + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use binary encoding when :internal_encoding option is specified" do + @io = IO.send(@method, @fd, 'wb', internal_encoding: "ibm866") + @io.internal_encoding.to_s.should == 'IBM866' + end + + it "does not use binary encoding when mode encoding is specified along with binmode: true option" do + @io = IO.send(@method, @fd, 'w:iso-8859-1', binmode: true) + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use textmode argument when mode encoding is specified" do + @io = IO.send(@method, @fd, 'w:ascii-8bit', textmode: true) + @io.external_encoding.to_s.should == 'ASCII-8BIT' + end + + it "does not use binmode argument when external encoding is specified via the :external_encoding option" do + @io = IO.send(@method, @fd, 'w', binmode: true, external_encoding: 'iso-8859-1') + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use textmode argument when external encoding is specified via the :external_encoding option" do + @io = IO.send(@method, @fd, 'w', textmode: true, external_encoding: 'ascii-8bit') + @io.external_encoding.to_s.should == 'ASCII-8BIT' + end + + it "raises ArgumentError for nil options" do + -> { + IO.send(@method, @fd, 'w', nil) + }.should raise_error(ArgumentError) + end + + it "coerces mode with #to_str" do + mode = mock("mode") + mode.should_receive(:to_str).and_return('w') + @io = IO.send(@method, @fd, mode) + end + + it "coerces mode with #to_int" do + mode = mock("mode") + mode.should_receive(:to_int).and_return(File::WRONLY) + @io = IO.send(@method, @fd, mode) + end + + it "coerces mode with #to_str when passed in options" do + mode = mock("mode") + mode.should_receive(:to_str).and_return('w') + @io = IO.send(@method, @fd, mode: mode) + end + + it "coerces mode with #to_int when passed in options" do + mode = mock("mode") + mode.should_receive(:to_int).and_return(File::WRONLY) + @io = IO.send(@method, @fd, mode: mode) + end + + it "coerces :encoding option with #to_str" do + encoding = mock("encoding") + encoding.should_receive(:to_str).and_return('utf-8') + @io = IO.send(@method, @fd, 'w', encoding: encoding) + end + + it "coerces :external_encoding option with #to_str" do + encoding = mock("encoding") + encoding.should_receive(:to_str).and_return('utf-8') + @io = IO.send(@method, @fd, 'w', external_encoding: encoding) + end + + it "coerces :internal_encoding option with #to_str" do + encoding = mock("encoding") + encoding.should_receive(:to_str).at_least(:once).and_return('utf-8') + @io = IO.send(@method, @fd, 'w', internal_encoding: encoding) + end + + it "coerces options as third argument with #to_hash" do + options = mock("options") + options.should_receive(:to_hash).and_return({}) + @io = IO.send(@method, @fd, 'w', **options) + end + + it "coerces options as second argument with #to_hash" do + options = mock("options") + options.should_receive(:to_hash).and_return({}) + @io = IO.send(@method, @fd, **options) + end + + it "accepts an :autoclose option" do + @io = IO.send(@method, @fd, 'w', autoclose: false) + @io.should_not.autoclose? + @io.autoclose = true + end + + it "accepts any truthy option :autoclose" do + @io = IO.send(@method, @fd, 'w', autoclose: 42) + @io.should.autoclose? + end +end + +# This group of specs may ONLY contain specs that do not actually create +# an IO instance from the file descriptor returned by #new_fd helper. +describe :io_new_errors, shared: true do + before :each do + @name = tmp("io_new.txt") + @fd = new_fd @name + end + + after :each do + IO.new(@fd, "w").close if @fd + rm_r @name + end + + it "raises an Errno::EBADF if the file descriptor is not valid" do + -> { IO.send(@method, -1, "w") }.should raise_error(Errno::EBADF) + end + + it "raises an IOError if passed a closed stream" do + -> { IO.send(@method, IOSpecs.closed_io.fileno, 'w') }.should raise_error(IOError) + end + + platform_is_not :windows do + it "raises an Errno::EINVAL if the new mode is not compatible with the descriptor's current mode" do + -> { IO.send(@method, @fd, "r") }.should raise_error(Errno::EINVAL) + end + end + + it "raises ArgumentError if passed an empty mode string" do + -> { IO.send(@method, @fd, "") }.should raise_error(ArgumentError) + end + + it "raises an error if passed modes two ways" do + -> { + IO.send(@method, @fd, "w", mode: "w") + }.should raise_error(ArgumentError) + end + + it "raises an error if passed encodings two ways" do + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', external_encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', internal_encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1:UTF-8', internal_encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + end + + it "raises an error if passed matching binary/text mode two ways" do + -> { + @io = IO.send(@method, @fd, "wb", binmode: true) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", textmode: true) + }.should raise_error(ArgumentError) + + -> { + @io = IO.send(@method, @fd, "wb", textmode: false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", binmode: false) + }.should raise_error(ArgumentError) + end + + it "raises an error if passed conflicting binary/text mode two ways" do + -> { + @io = IO.send(@method, @fd, "wb", binmode: false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", textmode: false) + }.should raise_error(ArgumentError) + + -> { + @io = IO.send(@method, @fd, "wb", textmode: true) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", binmode: true) + }.should raise_error(ArgumentError) + end + + it "raises an error when trying to set both binmode and textmode" do + -> { + @io = IO.send(@method, @fd, "w", textmode: true, binmode: true) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, File::Constants::WRONLY, textmode: true, binmode: true) + }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if not passed a hash or nil for options" do + -> { + @io = IO.send(@method, @fd, 'w', false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, false, false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, nil, false) + }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if passed a hash for mode and nil for options" do + -> { + @io = IO.send(@method, @fd, {mode: 'w'}, nil) + }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/shared/pos.rb b/spec/ruby/core/io/shared/pos.rb new file mode 100644 index 0000000000..3fdd3eb2b3 --- /dev/null +++ b/spec/ruby/core/io/shared/pos.rb @@ -0,0 +1,78 @@ +describe :io_pos, shared: true do + before :each do + @fname = tmp('test.txt') + File.open(@fname, 'w') { |f| f.write "123" } + end + + after :each do + rm_r @fname + end + + it "gets the offset" do + File.open @fname do |f| + f.send(@method).should == 0 + f.read 1 + f.send(@method).should == 1 + f.read 2 + f.send(@method).should == 3 + end + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send(@method) }.should raise_error(IOError) + end + + it "resets #eof?" do + open @fname do |io| + io.read 1 + io.read 1 + io.send(@method) + io.should_not.eof? + end + end +end + +describe :io_set_pos, shared: true do + before :each do + @fname = tmp('test.txt') + File.open(@fname, 'w') { |f| f.write "123" } + end + + after :each do + rm_r @fname + end + + it "sets the offset" do + File.open @fname do |f| + val1 = f.read 1 + f.send @method, 0 + f.read(1).should == val1 + end + end + + it "converts arguments to Integers" do + File.open @fname do |io| + o = mock("o") + o.should_receive(:to_int).and_return(1) + + io.send @method, o + io.pos.should == 1 + end + end + + it "raises TypeError when cannot convert implicitly argument to Integer" do + File.open @fname do |io| + -> { io.send @method, Object.new }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + end + + it "does not accept Integers that don't fit in a C off_t" do + File.open @fname do |io| + -> { io.send @method, 2**128 }.should raise_error(RangeError) + end + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send @method, 0 }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/shared/readlines.rb b/spec/ruby/core/io/shared/readlines.rb new file mode 100644 index 0000000000..6c1fa11a59 --- /dev/null +++ b/spec/ruby/core/io/shared/readlines.rb @@ -0,0 +1,259 @@ +describe :io_readlines, shared: true do + it "raises TypeError if the first parameter is nil" do + -> { IO.send(@method, nil, &@object) }.should raise_error(TypeError) + end + + it "raises an Errno::ENOENT if the file does not exist" do + name = tmp("nonexistent.txt") + -> { IO.send(@method, name, &@object) }.should raise_error(Errno::ENOENT) + end + + it "yields a single string with entire content when the separator is nil" do + result = IO.send(@method, @name, nil, &@object) + (result ? result : ScratchPad.recorded).should == [IO.read(@name)] + end + + it "yields a sequence of paragraphs when the separator is an empty string" do + result = IO.send(@method, @name, "", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_empty_separator + end + + it "yields a sequence of lines without trailing newline characters when chomp is passed" do + result = IO.send(@method, @name, chomp: true, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_without_newline_characters + end +end + +describe :io_readlines_options_19, shared: true do + before :each do + @filename = tmp("io readlines options") + end + + after :each do + rm_r @filename + end + + describe "when passed name" do + it "calls #to_path to convert the name" do + name = mock("io name to_path") + name.should_receive(:to_path).and_return(@name) + IO.send(@method, name, &@object) + end + + it "defaults to $/ as the separator" do + result = IO.send(@method, @name, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end + end + + describe "when passed name, object" do + it "calls #to_str to convert the object to a separator" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + result = IO.send(@method, @name, sep, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator + end + + describe "when the object is an Integer" do + before :each do + @sep = $/ + end + + after :each do + suppress_warning {$/ = @sep} + end + + it "defaults to $/ as the separator" do + suppress_warning {$/ = " "} + result = IO.send(@method, @name, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "uses the object as a limit if it is an Integer" do + result = IO.send(@method, @name, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_limit + end + + it "ignores the object as a limit if it is negative" do + result = IO.send(@method, @name, -2, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { IO.send(@method, @name, 2**128, &@object) }.should raise_error(RangeError) + end + + ruby_bug "#18767", ""..."3.3" do + describe "when passed limit" do + it "raises ArgumentError when passed 0 as a limit" do + -> { IO.send(@method, @name, 0, &@object) }.should raise_error(ArgumentError) + end + end + end + end + + describe "when the object is a String" do + it "uses the value as the separator" do + result = IO.send(@method, @name, " ", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator + end + + it "accepts non-ASCII data as separator" do + result = IO.send(@method, @name, "\303\250".dup.force_encoding("utf-8"), &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_arbitrary_separator + end + end + + describe "when the object is an options Hash" do + it "raises TypeError exception" do + -> { + IO.send(@method, @name, { chomp: true }, &@object) + }.should raise_error(TypeError) + end + end + + describe "when the object is neither Integer nor String" do + it "raises TypeError exception" do + obj = mock("not io readlines limit") + + -> { + IO.send(@method, @name, obj, &@object) + }.should raise_error(TypeError) + end + end + end + + describe "when passed name, keyword arguments" do + it "uses the keyword arguments as options" do + result = IO.send(@method, @name, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end + end + + describe "when passed name, object, object" do + describe "when the first object is a String" do + it "uses the second object as a limit if it is an Integer" do + result = IO.send(@method, @name, " ", 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_int to convert the second object" do + limit = mock("io readlines limit") + limit.should_receive(:to_int).at_least(1).and_return(10) + result = IO.send(@method, @name, " ", limit, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + end + + describe "when the first object is not a String or Integer" do + it "calls #to_str to convert the object to a String" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + result = IO.send(@method, @name, sep, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "uses the second object as a limit if it is an Integer" do + result = IO.send(@method, @name, " ", 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_int to convert the second object" do + limit = mock("io readlines limit") + limit.should_receive(:to_int).at_least(1).and_return(10) + result = IO.send(@method, @name, " ", limit, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + end + + describe "when the second object is neither Integer nor String" do + it "raises TypeError exception" do + obj = mock("not io readlines limit") + + -> { + IO.send(@method, @name, " ", obj, &@object) + }.should raise_error(TypeError) + end + end + + describe "when the second object is an options Hash" do + it "raises TypeError exception" do + -> { + IO.send(@method, @name, "", { chomp: true }, &@object) + }.should raise_error(TypeError) + end + end + end + + describe "when passed name, object, keyword arguments" do + describe "when the first object is an Integer" do + it "uses the keyword arguments as options" do + -> do + IO.send(@method, @filename, 10, mode: "w", &@object) + end.should raise_error(IOError) + end + end + + describe "when the first object is a String" do + it "uses the keyword arguments as options" do + -> do + IO.send(@method, @filename, " ", mode: "w", &@object) + end.should raise_error(IOError) + end + end + + describe "when the first object is not a String or Integer" do + it "uses the keyword arguments as options" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + + -> do + IO.send(@method, @filename, sep, mode: "w", &@object) + end.should raise_error(IOError) + end + end + end + + describe "when passed name, separator, limit, keyword arguments" do + it "calls #to_path to convert the name object" do + name = mock("io name to_path") + name.should_receive(:to_path).and_return(@name) + result = IO.send(@method, name, " ", 10, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_str to convert the separator object" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + result = IO.send(@method, @name, sep, 10, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_int to convert the limit argument" do + limit = mock("io readlines limit") + limit.should_receive(:to_int).at_least(1).and_return(10) + result = IO.send(@method, @name, " ", limit, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "uses the keyword arguments as options" do + -> do + IO.send(@method, @filename, " ", 10, mode: "w", &@object) + end.should raise_error(IOError) + end + + describe "when passed chomp, nil as a separator, and a limit" do + it "yields each line of limit size without truncating trailing new line character" do + # 43 - is a size of the 1st paragraph in the file + result = IO.send(@method, @name, nil, 43, chomp: true, &@object) + + (result ? result : ScratchPad.recorded).should == [ + "Voici la ligne une.\nQui è la linea due.\n\n\n", + "Aquí está la línea tres.\n" + "Hier ist Zeile ", + "vier.\n\nEstá aqui a linha cinco.\nHere is li", + "ne six.\n" + ] + end + end + end +end diff --git a/spec/ruby/core/io/shared/tty.rb b/spec/ruby/core/io/shared/tty.rb new file mode 100644 index 0000000000..89ac08ec86 --- /dev/null +++ b/spec/ruby/core/io/shared/tty.rb @@ -0,0 +1,24 @@ +require_relative '../fixtures/classes' + +describe :io_tty, shared: true do + platform_is_not :windows do + it "returns true if this stream is a terminal device (TTY)" do + begin + # check to enabled tty + File.open('/dev/tty') {} + rescue Errno::ENXIO + skip "workaround for not configured environment like OS X" + else + File.open('/dev/tty') { |f| f.send(@method) }.should == true + end + end + end + + it "returns false if this stream is not a terminal device (TTY)" do + File.open(__FILE__) { |f| f.send(@method) }.should == false + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send @method }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/shared/write.rb b/spec/ruby/core/io/shared/write.rb new file mode 100644 index 0000000000..964064746a --- /dev/null +++ b/spec/ruby/core/io/shared/write.rb @@ -0,0 +1,154 @@ +# encoding: utf-8 +require_relative '../fixtures/classes' + +describe :io_write, shared: true do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.send(@method, "012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @readonly_file.close if @readonly_file + @file.close if @file + rm_r @filename + end + + it "coerces the argument to a string using to_s" do + (obj = mock('test')).should_receive(:to_s).and_return('a string') + @file.send(@method, obj) + end + + it "checks if the file is writable if writing more than zero bytes" do + -> { @readonly_file.send(@method, "abcde") }.should raise_error(IOError) + end + + it "returns the number of bytes written" do + written = @file.send(@method, "abcde") + written.should == 5 + end + + it "invokes to_s on non-String argument" do + data = "abcdefgh9876" + (obj = mock(data)).should_receive(:to_s).and_return(data) + @file.send(@method, obj) + @file.seek(0) + @file.read(data.size).should == data + end + + it "writes all of the string's bytes without buffering if mode is sync" do + @file.sync = true + written = @file.send(@method, "abcde") + written.should == 5 + File.open(@filename) do |file| + file.read(10).should == "abcde56789" + end + end + + it "does not warn if called after IO#read" do + @file.read(5) + -> { @file.send(@method, "fghij") }.should_not complain + end + + it "writes to the current position after IO#read" do + @file.read(5) + @file.send(@method, "abcd") + @file.rewind + @file.read.should == "01234abcd901234567890123456789" + end + + it "advances the file position by the count of given bytes" do + @file.send(@method, "abcde") + @file.read(10).should == "5678901234" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send(@method, "hello") }.should raise_error(IOError) + end + + describe "on a pipe" do + before :each do + @r, @w = IO.pipe + end + + after :each do + @r.close + @w.close + end + + it "writes the given String to the pipe" do + @w.send(@method, "foo") + @w.close + @r.read.should == "foo" + end + + # [ruby-core:90895] RJIT worker may leave fd open in a forked child. + # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens, + # and the RJIT worker keeps the pipe open until the worker execve(). + # TODO: consider acquiring GVL from RJIT worker. + guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do + it "raises Errno::EPIPE if the read end is closed and does not die from SIGPIPE" do + @r.close + -> { @w.send(@method, "foo") }.should raise_error(Errno::EPIPE, /Broken pipe/) + end + end + end +end + +describe :io_write_transcode, shared: true do + before :each do + @transcode_filename = tmp("io_write_transcode") + end + + after :each do + rm_r @transcode_filename + end + + it "transcodes the given string when the external encoding is set and neither is BINARY" do + utf8_str = "hello" + + File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file| + file.external_encoding.should == Encoding::UTF_16BE + file.send(@method, utf8_str) + end + + result = File.binread(@transcode_filename) + expected = [0, 104, 0, 101, 0, 108, 0, 108, 0, 111] # UTF-16BE bytes for "hello" + + result.bytes.should == expected + end + + it "transcodes the given string when the external encoding is set and the string encoding is BINARY" do + str = "été".b + + File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file| + file.external_encoding.should == Encoding::UTF_16BE + -> { file.send(@method, str) }.should raise_error(Encoding::UndefinedConversionError) + end + end +end + +describe :io_write_no_transcode, shared: true do + before :each do + @transcode_filename = tmp("io_write_no_transcode") + end + + after :each do + rm_r @transcode_filename + end + + it "does not transcode the given string even when the external encoding is set" do + utf8_str = "hello" + + File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file| + file.external_encoding.should == Encoding::UTF_16BE + file.send(@method, utf8_str) + end + + result = File.binread(@transcode_filename) + result.bytes.should == utf8_str.bytes + end +end diff --git a/spec/ruby/core/io/stat_spec.rb b/spec/ruby/core/io/stat_spec.rb new file mode 100644 index 0000000000..717c45d0a3 --- /dev/null +++ b/spec/ruby/core/io/stat_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#stat" do + before :each do + cmd = platform_is(:windows) ? 'rem' : 'cat' + @io = IO.popen cmd, "r+" + end + + after :each do + @io.close unless @io.closed? + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.stat }.should raise_error(IOError) + end + + it "returns a File::Stat object for the stream" do + STDOUT.stat.should be_an_instance_of(File::Stat) + end + + it "can stat pipes" do + @io.stat.should be_an_instance_of(File::Stat) + end +end diff --git a/spec/ruby/core/io/sync_spec.rb b/spec/ruby/core/io/sync_spec.rb new file mode 100644 index 0000000000..993b7ee244 --- /dev/null +++ b/spec/ruby/core/io/sync_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#sync=" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "sets the sync mode to true or false" do + @io.sync = true + @io.sync.should == true + @io.sync = false + @io.sync.should == false + end + + it "accepts non-boolean arguments" do + @io.sync = 10 + @io.sync.should == true + @io.sync = nil + @io.sync.should == false + @io.sync = Object.new + @io.sync.should == true + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.sync = true }.should raise_error(IOError) + end +end + +describe "IO#sync" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns the current sync mode" do + @io.sync.should == false + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.sync }.should raise_error(IOError) + end +end + +describe "IO#sync" do + it "is false by default for STDIN" do + STDIN.sync.should == false + end + + it "is false by default for STDOUT" do + STDOUT.sync.should == false + end + + it "is true by default for STDERR" do + STDERR.sync.should == true + end +end diff --git a/spec/ruby/core/io/sysopen_spec.rb b/spec/ruby/core/io/sysopen_spec.rb new file mode 100644 index 0000000000..7ad379df3a --- /dev/null +++ b/spec/ruby/core/io/sysopen_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' + +describe "IO.sysopen" do + before :each do + @filename = tmp("rubinius-spec-io-sysopen-#{$$}.txt") + @fd = nil + end + + after :each do + IO.for_fd(@fd).close if @fd + rm_r @filename + end + + it "returns the file descriptor for a given path" do + @fd = IO.sysopen(@filename, "w") + @fd.should be_kind_of(Integer) + @fd.should_not equal(0) + end + + # opening a directory is not supported on Windows + platform_is_not :windows do + it "works on directories" do + @fd = IO.sysopen(tmp("")) # /tmp + @fd.should be_kind_of(Integer) + @fd.should_not equal(0) + end + end + + it "calls #to_path to convert an object to a path" do + path = mock('sysopen to_path') + path.should_receive(:to_path).and_return(@filename) + @fd = IO.sysopen(path, 'w') + end + + it "accepts a mode as second argument" do + -> { @fd = IO.sysopen(@filename, "w") }.should_not raise_error + @fd.should_not equal(0) + end + + it "accepts permissions as third argument" do + @fd = IO.sysopen(@filename, "w", 777) + @fd.should_not equal(0) + end + + it "accepts mode & permission that are nil" do + touch @filename # create the file + @fd = IO.sysopen(@filename, nil, nil) + @fd.should_not equal(0) + end +end diff --git a/spec/ruby/core/io/sysread_spec.rb b/spec/ruby/core/io/sysread_spec.rb new file mode 100644 index 0000000000..d56a27b3af --- /dev/null +++ b/spec/ruby/core/io/sysread_spec.rb @@ -0,0 +1,137 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#sysread on a file" do + before :each do + @file_name = tmp("IO_sysread_file") + $$.to_s + File.open(@file_name, "w") do |f| + # write some stuff + f.write("012345678901234567890123456789\nabcdef") + end + @file = File.open(@file_name, "r+") + end + + after :each do + @file.close + rm_r @file_name + end + + it "reads the specified number of bytes from the file" do + @file.sysread(15).should == "012345678901234" + end + + it "reads the specified number of bytes from the file to the buffer" do + buf = +"" # empty buffer + @file.sysread(15, buf).should == buf + buf.should == "012345678901234" + + @file.rewind + + buf = +"ABCDE" # small buffer + @file.sysread(15, buf).should == buf + buf.should == "012345678901234" + + @file.rewind + + buf = +"ABCDE" * 5 # large buffer + @file.sysread(15, buf).should == buf + buf.should == "012345678901234" + end + + it "coerces the second argument to string and uses it as a buffer" do + buf = +"ABCDE" + (obj = mock("buff")).should_receive(:to_str).any_number_of_times.and_return(buf) + @file.sysread(15, obj).should == buf + buf.should == "012345678901234" + end + + it "advances the position of the file by the specified number of bytes" do + @file.sysread(15) + @file.sysread(5).should == "56789" + end + + it "raises an error when called after buffered reads" do + @file.readline + -> { @file.sysread(5) }.should raise_error(IOError) + end + + it "reads normally even when called immediately after a buffered IO#read" do + @file.read(15) + @file.sysread(5).should == "56789" + end + + it "does not raise error if called after IO#read followed by IO#write" do + @file.read(5) + @file.write("abcde") + -> { @file.sysread(5) }.should_not raise_error(IOError) + end + + it "does not raise error if called after IO#read followed by IO#syswrite" do + @file.read(5) + @file.syswrite("abcde") + -> { @file.sysread(5) }.should_not raise_error(IOError) + end + + it "reads updated content after the flushed buffered IO#write" do + @file.write("abcde") + @file.flush + @file.sysread(5).should == "56789" + File.open(@file_name) do |f| + f.sysread(10).should == "abcde56789" + end + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.sysread(5) }.should raise_error(IOError) + end + + it "immediately returns an empty string if the length argument is 0" do + @file.sysread(0).should == "" + end + + it "immediately returns the given buffer if the length argument is 0" do + buffer = +"existing content" + @file.sysread(0, buffer).should == buffer + buffer.should == "existing content" + end + + it "discards the existing buffer content upon successful read" do + buffer = +"existing content" + @file.sysread(11, buffer).should.equal?(buffer) + buffer.should == "01234567890" + end + + it "discards the existing buffer content upon error" do + buffer = +"existing content" + @file.seek(0, :END) + -> { @file.sysread(1, buffer) }.should raise_error(EOFError) + buffer.should be_empty + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + string = @file.sysread(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end +end + +describe "IO#sysread" do + before do + @read, @write = IO.pipe + end + + after do + @read.close + @write.close + end + + it "returns a smaller string if less than size bytes are available" do + @write.syswrite "ab" + @read.sysread(3).should == "ab" + end + + it "raises ArgumentError when length is less than 0" do + -> { @read.sysread(-1) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/sysseek_spec.rb b/spec/ruby/core/io/sysseek_spec.rb new file mode 100644 index 0000000000..002f2a14eb --- /dev/null +++ b/spec/ruby/core/io/sysseek_spec.rb @@ -0,0 +1,49 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#sysseek" do + it_behaves_like :io_set_pos, :sysseek +end + +describe "IO#sysseek" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "moves the read position relative to the current position with SEEK_CUR" do + @io.sysseek(10, IO::SEEK_CUR) + @io.readline.should == "igne une.\n" + end + + it "raises an error when called after buffered reads" do + @io.readline + -> { @io.sysseek(-5, IO::SEEK_CUR) }.should raise_error(IOError) + end + + it "seeks normally even when called immediately after a buffered IO#read" do + @io.read(15) + @io.sysseek(-5, IO::SEEK_CUR).should == 10 + end + + it "moves the read position relative to the start with SEEK_SET" do + @io.sysseek(43, IO::SEEK_SET) + @io.readline.should == "Aquí está la línea tres.\n" + end + + it "moves the read position relative to the end with SEEK_END" do + @io.sysseek(1, IO::SEEK_END) + + # this is the safest way of checking the EOF when + # sys-* methods are invoked + -> { @io.sysread(1) }.should raise_error(EOFError) + + @io.sysseek(-25, IO::SEEK_END) + @io.sysread(7).should == "cinco.\n" + end +end diff --git a/spec/ruby/core/io/syswrite_spec.rb b/spec/ruby/core/io/syswrite_spec.rb new file mode 100644 index 0000000000..8bf61a27c3 --- /dev/null +++ b/spec/ruby/core/io/syswrite_spec.rb @@ -0,0 +1,82 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/write' + +describe "IO#syswrite on a file" do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.syswrite("012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @file.close + @readonly_file.close + rm_r @filename + end + + it "writes all of the string's bytes but does not buffer them" do + written = @file.syswrite("abcde") + written.should == 5 + File.open(@filename) do |file| + file.sysread(10).should == "abcde56789" + file.seek(0) + @file.fsync + file.sysread(10).should == "abcde56789" + end + end + + it "does not modify the passed argument" do + File.open(@filename, "w") do |f| + f.set_encoding(Encoding::IBM437) + # A character whose codepoint differs between UTF-8 and IBM437 + f.syswrite("ƒ".freeze) + end + + File.binread(@filename).bytes.should == [198, 146] + end + + it "warns if called immediately after a buffered IO#write" do + @file.write("abcde") + -> { @file.syswrite("fghij") }.should complain(/syswrite/) + end + + it "does not warn if called after IO#write with intervening IO#sysread" do + @file.syswrite("abcde") + @file.sysread(5) + -> { @file.syswrite("fghij") }.should_not complain + end + + it "writes to the actual file position when called after buffered IO#read" do + @file.read(5) + @file.syswrite("abcde") + File.open(@filename) do |file| + file.sysread(10).should == "01234abcde" + end + end +end + +describe "IO#syswrite on a pipe" do + it "returns the written bytes if the fd is in nonblock mode and write would block" do + require 'io/nonblock' + r, w = IO.pipe + begin + w.nonblock = true + larger_than_pipe_capacity = 2 * 1024 * 1024 + written = w.syswrite("a"*larger_than_pipe_capacity) + written.should > 0 + written.should < larger_than_pipe_capacity + ensure + w.close + r.close + end + end +end + +describe "IO#syswrite" do + it_behaves_like :io_write, :syswrite + it_behaves_like :io_write_no_transcode, :syswrite +end diff --git a/spec/ruby/core/io/tell_spec.rb b/spec/ruby/core/io/tell_spec.rb new file mode 100644 index 0000000000..0d6c6b02d3 --- /dev/null +++ b/spec/ruby/core/io/tell_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#tell" do + it_behaves_like :io_pos, :tell +end diff --git a/spec/ruby/core/io/to_i_spec.rb b/spec/ruby/core/io/to_i_spec.rb new file mode 100644 index 0000000000..acf138c663 --- /dev/null +++ b/spec/ruby/core/io/to_i_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#to_i" do + it "returns the numeric file descriptor of the given IO object" do + $stdout.to_i.should == 1 + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.to_i }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/to_io_spec.rb b/spec/ruby/core/io/to_io_spec.rb new file mode 100644 index 0000000000..55a0977740 --- /dev/null +++ b/spec/ruby/core/io/to_io_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#to_io" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns self for open stream" do + @io.to_io.should equal(@io) + end + + it "returns self for closed stream" do + io = IOSpecs.closed_io + io.to_io.should equal(io) + end +end diff --git a/spec/ruby/core/io/try_convert_spec.rb b/spec/ruby/core/io/try_convert_spec.rb new file mode 100644 index 0000000000..a9e99de7aa --- /dev/null +++ b/spec/ruby/core/io/try_convert_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.try_convert" do + before :each do + @name = tmp("io_try_convert.txt") + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "returns the passed IO object" do + IO.try_convert(@io).should equal(@io) + end + + it "does not call #to_io on an IO instance" do + @io.should_not_receive(:to_io) + IO.try_convert(@io) + end + + it "calls #to_io to coerce an object" do + obj = mock("io") + obj.should_receive(:to_io).and_return(@io) + IO.try_convert(obj).should equal(@io) + end + + it "returns nil when the passed object does not respond to #to_io" do + IO.try_convert(mock("io")).should be_nil + end + + it "return nil when BasicObject is passed" do + IO.try_convert(BasicObject.new).should be_nil + end + + it "raises a TypeError if the object does not return an IO from #to_io" do + obj = mock("io") + obj.should_receive(:to_io).and_return("io") + -> { IO.try_convert(obj) }.should raise_error(TypeError, "can't convert MockObject to IO (MockObject#to_io gives String)") + end + + it "propagates an exception raised by #to_io" do + obj = mock("io") + obj.should_receive(:to_io).and_raise(TypeError.new) + ->{ IO.try_convert(obj) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/io/tty_spec.rb b/spec/ruby/core/io/tty_spec.rb new file mode 100644 index 0000000000..3b76c6d2b8 --- /dev/null +++ b/spec/ruby/core/io/tty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/tty' + +describe "IO#tty?" do + it_behaves_like :io_tty, :tty? +end diff --git a/spec/ruby/core/io/ungetbyte_spec.rb b/spec/ruby/core/io/ungetbyte_spec.rb new file mode 100644 index 0000000000..716743a6af --- /dev/null +++ b/spec/ruby/core/io/ungetbyte_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' + +describe "IO#ungetbyte" do + before :each do + @name = tmp("io_ungetbyte") + touch(@name) { |f| f.write "a" } + @io = new_io @name, "r" + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "does nothing when passed nil" do + @io.ungetbyte(nil).should be_nil + @io.getbyte.should == 97 + end + + it "puts back each byte in a String argument" do + @io.ungetbyte("cat").should be_nil + @io.getbyte.should == 99 + @io.getbyte.should == 97 + @io.getbyte.should == 116 + @io.getbyte.should == 97 + end + + it "calls #to_str to convert the argument" do + str = mock("io ungetbyte") + str.should_receive(:to_str).and_return("dog") + + @io.ungetbyte(str).should be_nil + @io.getbyte.should == 100 + @io.getbyte.should == 111 + @io.getbyte.should == 103 + @io.getbyte.should == 97 + end + + it "never raises RangeError" do + for i in [4095, 0x4f7574206f6620636861722072616e67ff] do + @io.ungetbyte(i).should be_nil + @io.getbyte.should == 255 + end + end + + it "raises IOError on stream not opened for reading" do + -> { STDOUT.ungetbyte(42) }.should raise_error(IOError, "not opened for reading") + end + + it "raises an IOError if the IO is closed" do + @io.close + -> { @io.ungetbyte(42) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/ungetc_spec.rb b/spec/ruby/core/io/ungetc_spec.rb new file mode 100644 index 0000000000..47a4e99ebf --- /dev/null +++ b/spec/ruby/core/io/ungetc_spec.rb @@ -0,0 +1,138 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#ungetc" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + + @empty = tmp('empty.txt') + end + + after :each do + @io.close unless @io.closed? + rm_r @empty + end + + it "pushes back one character onto stream" do + @io.getc.should == ?V + @io.ungetc(86) + @io.getc.should == ?V + + @io.ungetc(10) + @io.getc.should == ?\n + + @io.getc.should == ?o + @io.getc.should == ?i + # read the rest of line + @io.readline.should == "ci la ligne une.\n" + @io.getc.should == ?Q + @io.ungetc(99) + @io.getc.should == ?c + end + + it "interprets the codepoint in the external encoding" do + @io.set_encoding(Encoding::UTF_8) + @io.ungetc(233) + c = @io.getc + c.encoding.should == Encoding::UTF_8 + c.should == "é" + c.bytes.should == [195, 169] + + @io.set_encoding(Encoding::IBM437) + @io.ungetc(130) + c = @io.getc + c.encoding.should == Encoding::IBM437 + c.bytes.should == [130] + c.encode(Encoding::UTF_8).should == "é" + end + + it "pushes back one character when invoked at the end of the stream" do + # read entire content + @io.read + @io.ungetc(100) + @io.getc.should == ?d + end + + it "pushes back one character when invoked at the start of the stream" do + @io.read(0) + @io.ungetc(100) + @io.getc.should == ?d + end + + it "pushes back one character when invoked on empty stream" do + touch(@empty) + + File.open(@empty) { |empty| + empty.getc().should == nil + empty.ungetc(10) + empty.getc.should == ?\n + } + end + + it "affects EOF state" do + touch(@empty) + + File.open(@empty) { |empty| + empty.should.eof? + empty.getc.should == nil + empty.ungetc(100) + empty.should_not.eof? + } + end + + it "adjusts the stream position" do + @io.pos.should == 0 + + # read one char + c = @io.getc + @io.pos.should == 1 + @io.ungetc(c) + @io.pos.should == 0 + + # read all + @io.read + pos = @io.pos + @io.ungetc(98) + @io.pos.should == pos - 1 + end + + it "makes subsequent unbuffered operations to raise IOError" do + @io.getc + @io.ungetc(100) + -> { @io.sysread(1) }.should raise_error(IOError) + end + + it "raises TypeError if passed nil" do + @io.getc.should == ?V + proc{@io.ungetc(nil)}.should raise_error(TypeError) + end + + it "puts one or more characters back in the stream" do + @io.gets + @io.ungetc("Aquí ").should be_nil + @io.gets.chomp.should == "Aquí Qui è la linea due." + 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í ") + + @io.ungetc(chars).should be_nil + @io.gets.chomp.should == "Aquí Voici la ligne une." + end + + it "returns nil when invoked on stream that was not yet read" do + @io.ungetc(100).should be_nil + end + + it "raises IOError on stream not opened for reading" do + -> { STDOUT.ungetc(100) }.should raise_error(IOError, "not opened for reading") + end + + it "raises IOError on closed stream" do + @io.getc + @io.close + -> { @io.ungetc(100) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/write_nonblock_spec.rb b/spec/ruby/core/io/write_nonblock_spec.rb new file mode 100644 index 0000000000..5bfc690f9b --- /dev/null +++ b/spec/ruby/core/io/write_nonblock_spec.rb @@ -0,0 +1,96 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/write' + +# See https://bugs.ruby-lang.org/issues/5954#note-5 +platform_is_not :windows do + describe "IO#write_nonblock on a file" do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.write_nonblock("012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @file.close if @file + @readonly_file.close if @readonly_file + rm_r @filename + end + + it "writes all of the string's bytes but does not buffer them" do + written = @file.write_nonblock("abcde") + written.should == 5 + File.open(@filename) do |file| + file.sysread(10).should == "abcde56789" + file.seek(0) + @file.fsync + file.sysread(10).should == "abcde56789" + end + end + + it "does not modify the passed argument" do + File.open(@filename, "w") do |f| + f.set_encoding(Encoding::IBM437) + # A character whose codepoint differs between UTF-8 and IBM437 + f.write_nonblock("ƒ".freeze) + end + + File.binread(@filename).bytes.should == [198, 146] + end + + it "checks if the file is writable if writing zero bytes" do + -> { + @readonly_file.write_nonblock("") + }.should raise_error(IOError) + end + end + + describe "IO#write_nonblock" do + it_behaves_like :io_write, :write_nonblock + it_behaves_like :io_write_no_transcode, :write_nonblock + end +end + +describe 'IO#write_nonblock' do + before do + @read, @write = IO.pipe + end + + after do + @read.close + @write.close + end + + it "raises an exception extending IO::WaitWritable when the write would block" do + -> { + loop { @write.write_nonblock('a' * 10_000) } + }.should raise_error(IO::WaitWritable) { |e| + platform_is_not :windows do + e.should be_kind_of(Errno::EAGAIN) + end + platform_is :windows do + e.should be_kind_of(Errno::EWOULDBLOCK) + end + } + end + + context "when exception option is set to false" do + it "returns :wait_writable when the operation would block" do + loop { + break if @write.write_nonblock("a" * 10_000, exception: false) == :wait_writable + } + @write.write_nonblock("a" * 10_000, exception: false).should == :wait_writable + end + end + + platform_is_not :windows do + it 'sets the IO in nonblock mode' do + require 'io/nonblock' + @write.write_nonblock('a') + @write.should.nonblock? + end + end +end diff --git a/spec/ruby/core/io/write_spec.rb b/spec/ruby/core/io/write_spec.rb new file mode 100644 index 0000000000..e58100f846 --- /dev/null +++ b/spec/ruby/core/io/write_spec.rb @@ -0,0 +1,297 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/write' +require_relative 'shared/binwrite' + +describe "IO#write on a file" do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.write("012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @file.close + @readonly_file.close + rm_r @filename + end + + it "does not check if the file is writable if writing zero bytes" do + -> { @readonly_file.write("") }.should_not raise_error + end + + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::UTF_8 + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "returns a length of 0 when writing a blank string" do + @file.write('').should == 0 + end + + it "returns a length of 0 when writing blank strings" do + @file.write('', '', '').should == 0 + end + + it "returns a length of 0 when passed no arguments" do + @file.write().should == 0 + end + + it "returns the number of bytes written" do + @file.write("hellø").should == 6 + end + + it "does not modify the passed argument" do + File.open(@filename, "w") do |f| + f.set_encoding(Encoding::IBM437) + # A character whose codepoint differs between UTF-8 and IBM437 + f.write("ƒ".freeze) + end + + File.binread(@filename).bytes.should == [159] + end + + it "does not modify arguments when passed multiple arguments and external encoding not set" do + a, b = "a".freeze, "b".freeze + + File.open(@filename, "w") do |f| + f.write(a, b) + end + + File.binread(@filename).bytes.should == [97, 98] + a.encoding.should == Encoding::UTF_8 + b.encoding.should == Encoding::UTF_8 + end + + it "uses the encoding from the given option for non-ascii encoding" do + File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file| + file.write("hi").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + end + + it "uses the encoding from the given option for non-ascii encoding even if in binary mode" do + File.open(@filename, "w", encoding: Encoding::UTF_32LE, binmode: true) do |file| + file.should.binmode? + file.write("hi").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + + File.open(@filename, "wb", encoding: Encoding::UTF_32LE) do |file| + file.should.binmode? + file.write("hi").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + end + + it "uses the encoding from the given option for non-ascii encoding when multiple arguments passes" do + File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file| + file.write("h", "i").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + end + + it "raises a invalid byte sequence error if invalid bytes are being written" do + # pack "\xFEhi" to avoid utf-8 conflict + xFEhi = ([254].pack('C*') + 'hi').force_encoding('utf-8') + File.open(@filename, "w", encoding: Encoding::US_ASCII) do |file| + -> { file.write(xFEhi) }.should raise_error(Encoding::InvalidByteSequenceError) + end + end + + it "writes binary data if no encoding is given" do + File.open(@filename, "w") do |file| + file.write('Hëllö'.encode('ISO-8859-1')) + end + ë = ([235].pack('U')).encode('ISO-8859-1') + ö = ([246].pack('U')).encode('ISO-8859-1') + res = "H#{ë}ll#{ö}" + File.binread(@filename).should == res.force_encoding(Encoding::BINARY) + end + + platform_is_not :windows do + it "writes binary data if no encoding is given and multiple arguments passed" do + File.open(@filename, "w") do |file| + file.write("\x87".b, "ą") # 0x87 isn't a valid UTF-8 binary representation of a character + end + File.binread(@filename).bytes.should == [0x87, 0xC4, 0x85] + + File.open(@filename, "w") do |file| + file.write("\x61".encode("utf-32le"), "ą") + end + File.binread(@filename).bytes.should == [0x61, 0x00, 0x00, 0x00, 0xC4, 0x85] + end + end +end + +describe "IO.write" do + it_behaves_like :io_binwrite, :write + + it "uses an :open_args option" do + IO.write(@filename, 'hi', open_args: ["w", nil, {encoding: Encoding::UTF_32LE}]).should == 8 + end + + it "disregards other options if :open_args is given" do + IO.write(@filename, 'hi', 2, mode: "r", encoding: Encoding::UTF_32LE, open_args: ["w"]).should == 2 + File.read(@filename).should == "\0\0hi" + end + + it "requires mode to be specified in :open_args" do + -> { + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true}]) + }.should raise_error(IOError, "not opened for writing") + + IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true}]).should == 8 + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, mode: "w"}]).should == 8 + end + + it "requires mode to be specified in :open_args even if flags option passed" do + -> { + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}]) + }.should raise_error(IOError, "not opened for writing") + + IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}]).should == 8 + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT, mode: "w"}]).should == 8 + end + + it "uses the given encoding and returns the number of bytes written" do + IO.write(@filename, 'hi', mode: "w", encoding: Encoding::UTF_32LE).should == 8 + end + + it "raises ArgumentError if encoding is specified in mode parameter and is given as :encoding option" do + -> { + IO.write(@filename, 'hi', mode: "w:UTF-16LE:UTF-16BE", encoding: Encoding::UTF_32LE) + }.should raise_error(ArgumentError, "encoding specified twice") + + -> { + IO.write(@filename, 'hi', mode: "w:UTF-16BE", encoding: Encoding::UTF_32LE) + }.should raise_error(ArgumentError, "encoding specified twice") + end + + it "writes the file with the permissions in the :perm parameter" do + rm_r @filename + IO.write(@filename, 'write :perm spec', mode: "w", perm: 0o755).should == 16 + (File.stat(@filename).mode & 0o777) == 0o755 + end + + it "writes binary data if no encoding is given" do + IO.write(@filename, 'Hëllö'.encode('ISO-8859-1')) + xEB = [235].pack('C*') + xF6 = [246].pack('C*') + File.binread(@filename).should == ("H" + xEB + "ll" + xF6).force_encoding(Encoding::BINARY) + end + + platform_is_not :windows do + describe "on a FIFO" do + before :each do + @fifo = tmp("File_open_fifo") + File.mkfifo(@fifo) + end + + after :each do + rm_r @fifo + end + + # rb_cloexec_open() is currently missing a retry on EINTR. + # @ioquatix is looking into fixing it. Quarantined until it's done. + quarantine! do + it "writes correctly" do + thr = Thread.new do + IO.read(@fifo) + end + begin + string = "hi" + IO.write(@fifo, string).should == string.length + ensure + thr.join + end + end + end + end + + ruby_version_is "3.3"..."4.0" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + -> { + -> { + IO.write("|cat", "xxx") + }.should output_to_fd("xxx") + }.should complain(/IO process creation with a leading '\|'/) + end + end + end +end + +describe "IO#write" do + it_behaves_like :io_write, :write + it_behaves_like :io_write_transcode, :write + + it "accepts multiple arguments" do + IO.pipe do |r, w| + w.write("foo", "bar") + w.close + + r.read.should == "foobar" + end + end +end + +platform_is :windows do + describe "IO#write on Windows" do + before :each do + @fname = tmp("io_write.txt") + end + + after :each do + rm_r @fname + @io.close if @io and !@io.closed? + end + + it "normalizes line endings in text mode" do + @io = new_io(@fname, "wt") + @io.write "a\nb\nc" + @io.close + File.binread(@fname).should == "a\r\nb\r\nc" + end + + it "does not normalize line endings in binary mode" do + @io = new_io(@fname, "wb") + @io.write "a\r\nb\r\nc" + @io.close + File.binread(@fname).should == "a\r\nb\r\nc" + end + end +end + +describe "IO#write on STDOUT" do + # https://bugs.ruby-lang.org/issues/14413 + platform_is_not :windows do + it "raises SignalException SIGPIPE if the stream is closed instead of Errno::EPIPE like other IOs" do + stderr_file = tmp("stderr") + begin + IO.popen([*ruby_exe, "-e", "loop { puts :ok }"], "r", err: stderr_file) do |io| + io.gets.should == "ok\n" + io.close + end + status = $? + status.should_not.success? + status.should.signaled? + Signal.signame(status.termsig).should == 'PIPE' + File.read(stderr_file).should.empty? + ensure + rm_r stderr_file + end + end + end +end |
