diff options
author | eregon <eregon@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-20 20:18:52 +0000 |
---|---|---|
committer | eregon <eregon@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-20 20:18:52 +0000 |
commit | 1d15d5f08032acf1b7bceacbb450d617ff6e0931 (patch) | |
tree | a3785a79899302bc149e4a6e72f624ac27dc1f10 /spec/ruby/core/io | |
parent | 75bfc6440d595bf339007f4fb280fd4d743e89c1 (diff) | |
download | ruby-1d15d5f08032acf1b7bceacbb450d617ff6e0931.tar.gz |
Move spec/rubyspec to spec/ruby for consistency
* Other ruby implementations use the spec/ruby directory.
[Misc #13792] [ruby-core:82287]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@59979 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'spec/ruby/core/io')
105 files changed, 7327 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..460b50a59d --- /dev/null +++ b/spec/ruby/core/io/advise_spec.rb @@ -0,0 +1,97 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { + @io.advise("normal") + }.should raise_error(TypeError) + end + + it "raises a TypeError if offsert cannot be coerced to an Integer" do + lambda { + @io.advise(:normal, "wat") + }.should raise_error(TypeError) + end + + it "raises a TypeError if len cannot be coerced to an Integer" do + lambda { + @io.advise(:normal, 0, "wat") + }.should raise_error(TypeError) + end + + it "raises a RangeError if offset is too big" do + lambda { + @io.advise(:normal, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a RangeError if len is too big" do + lambda { + @io.advise(:normal, 0, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a NotImplementedError if advise is not recognized" do + lambda{ + @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 + + platform_is :linux do + it "supports the willneed advice type" do + require 'etc' + uname = if Etc.respond_to?(:uname) + Etc.uname[:release] + else + `uname -r`.chomp + end + if (uname.split('.').map(&:to_i) <=> [3,6]) < 0 + # [ruby-core:65355] tmpfs is not supported + 1.should == 1 + else + @io.advise(:willneed).should be_nil + end + end + end + + it "raises an IOError if the stream is closed" do + @io.close + lambda { @io.advise(:normal) }.should raise_error(IOError) + 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..f437c8a4a4 --- /dev/null +++ b/spec/ruby/core/io/binmode_spec.rb @@ -0,0 +1,60 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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 +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..b592639f9d --- /dev/null +++ b/spec/ruby/core/io/binread_spec.rb @@ -0,0 +1,49 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 ASCII-8BIT encoding" do + IO.binread(@fname).encoding.should == Encoding::ASCII_8BIT + end + + it "returns a String in ASCII-8BIT encoding regardless of Encoding.default_internal" do + Encoding.default_internal = Encoding::EUC_JP + IO.binread(@fname).encoding.should == Encoding::ASCII_8BIT + end + + it "raises an ArgumentError when not passed a valid length" do + lambda { IO.binread @fname, -1 }.should raise_error(ArgumentError) + end + + ruby_version_is "2.3" do # MRI leaks the fd on older versions + it "raises an Errno::EINVAL when not passed a valid offset" do + lambda { IO.binread @fname, 0, -1 }.should raise_error(Errno::EINVAL) + 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..ec964d07a8 --- /dev/null +++ b/spec/ruby/core/io/binwrite_spec.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/binwrite', __FILE__) + +describe "IO.binwrite" do + it_behaves_like :io_binwrite, :binwrite + + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/io/bytes_spec.rb b/spec/ruby/core/io/bytes_spec.rb new file mode 100644 index 0000000000..3eb51883c4 --- /dev/null +++ b/spec/ruby/core/io/bytes_spec.rb @@ -0,0 +1,43 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#bytes" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns an enumerator of the next bytes from the stream" do + enum = @io.bytes + enum.should be_an_instance_of(Enumerator) + @io.readline.should == "Voici la ligne une.\n" + enum.first(5).should == [81, 117, 105, 32, 195] + end + + it "yields each byte" do + count = 0 + ScratchPad.record [] + @io.each_byte do |byte| + ScratchPad << byte + break if 4 < count += 1 + end + + ScratchPad.recorded.should == [86, 111, 105, 99, 105] + end + + it "raises an IOError on closed stream" do + enum = IOSpecs.closed_io.bytes + lambda { enum.first }.should raise_error(IOError) + end + + it "raises an IOError on an enumerator for a stream that has been closed" do + enum = @io.bytes + enum.first.should == 86 + @io.close + lambda { enum.first }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/chars_spec.rb b/spec/ruby/core/io/chars_spec.rb new file mode 100644 index 0000000000..e38160274f --- /dev/null +++ b/spec/ruby/core/io/chars_spec.rb @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/chars', __FILE__) + +describe "IO#chars" do + it_behaves_like :io_chars, :chars +end + +describe "IO#chars" do + it_behaves_like :io_chars_empty, :chars +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..057a9a1c20 --- /dev/null +++ b/spec/ruby/core/io/close_on_exec_spec.rb @@ -0,0 +1,100 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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 :windows and ruby_version_is ""..."2.3" } do + it "returns false from #respond_to?" do + @io.respond_to?(:close_on_exec=).should be_false + end + + it "raises a NotImplementedError when called" do + lambda { @io.close_on_exec = true }.should raise_error(NotImplementedError) + end + end + + guard -> { platform_is_not :windows or ruby_version_is "2.3" } do + it "sets the close-on-exec flag if true" do + @io.close_on_exec = true + @io.close_on_exec?.should == true + end + + it "sets the close-on-exec flag if non-false" do + @io.close_on_exec = :true + @io.close_on_exec?.should == true + end + + it "unsets the close-on-exec flag if false" do + @io.close_on_exec = true + @io.close_on_exec = false + @io.close_on_exec?.should == false + end + + it "unsets the close-on-exec flag if nil" do + @io.close_on_exec = true + @io.close_on_exec = nil + @io.close_on_exec?.should == false + 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 + lambda { @io.close_on_exec = true }.should raise_error(IOError) + end + + it "returns nil" do + @io.send(:close_on_exec=, true).should be_nil + 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 :windows and ruby_version_is ""..."2.3" } do + it "returns false from #respond_to?" do + @io.respond_to?(:close_on_exec?).should be_false + end + + it "raises a NotImplementedError when called" do + lambda { @io.close_on_exec? }.should raise_error(NotImplementedError) + end + end + + guard -> { platform_is_not :windows or ruby_version_is "2.3" } do + it "returns true by default" do + @io.close_on_exec?.should == true + end + + it "returns true if set" do + @io.close_on_exec = true + @io.close_on_exec?.should == true + end + + it "raises IOError if called on a closed IO" do + @io.close + lambda { @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..b5aba57795 --- /dev/null +++ b/spec/ruby/core/io/close_read_spec.rb @@ -0,0 +1,80 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#close_read" do + + before :each do + @io = IO.popen 'cat', "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 + + lambda { @io.read }.should raise_error(IOError) + end + + ruby_version_is ''...'2.3' do + it "raises an IOError on subsequent invocations" do + @io.close_read + + lambda { @io.close_read }.should raise_error(IOError) + end + end + + ruby_version_is '2.3' do + it "does nothing on subsequent invocations" do + @io.close_read + + @io.close_read.should be_nil + end + end + + it "allows subsequent invocation of close" do + @io.close_read + + lambda { @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 + lambda { 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.closed?.should == true + end + + ruby_version_is ''...'2.3' do + it "raises IOError on closed stream" do + @io.close + + lambda { @io.close_read }.should raise_error(IOError) + end + end + + ruby_version_is '2.3' do + it "does nothing on closed stream" do + @io.close + + @io.close_read.should be_nil + end + 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..0e51ec23d2 --- /dev/null +++ b/spec/ruby/core/io/close_spec.rb @@ -0,0 +1,82 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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.closed?.should == true + end + + it "returns nil" do + @io.close.should == nil + end + + it "raises an IOError reading from a closed IO" do + @io.close + lambda { @io.read }.should raise_error(IOError) + end + + it "raises an IOError writing to a closed IO" do + @io.close + lambda { @io.write "data" }.should raise_error(IOError) + end + + ruby_version_is ''...'2.3' do + it "raises an IOError if closed" do + @io.close + lambda { @io.close }.should raise_error(IOError) + end + end + + ruby_version_is "2.3" do + it "does nothing if already closed" do + @io.close + + @io.close.should be_nil + end + 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 + + lambda { 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..c901aac499 --- /dev/null +++ b/spec/ruby/core/io/close_write_spec.rb @@ -0,0 +1,84 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#close_write" do + before :each do + @io = IO.popen 'cat', '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 + + lambda { @io.write "attempt to write" }.should raise_error(IOError) + end + + ruby_version_is ''...'2.3' do + it "raises an IOError on subsequent invocations" do + @io.close_write + + lambda { @io.close_write }.should raise_error(IOError) + end + end + + ruby_version_is '2.3' do + it "does nothing on subsequent invocations" do + @io.close_write + + @io.close_write.should be_nil + end + end + + it "allows subsequent invocation of close" do + @io.close_write + + lambda { @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 + lambda { 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.closed?.should == true + end + + it "flushes and closes the write stream" do + @io.puts '12345' + + @io.close_write + + @io.read.should == "12345\n" + end + + ruby_version_is ''...'2.3' do + it "raises IOError on closed stream" do + @io.close + + lambda { @io.close_write }.should raise_error(IOError) + end + end + + ruby_version_is '2.3' do + it "does nothing on closed stream" do + @io.close + + @io.close_write.should be_nil + end + 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..80562885a7 --- /dev/null +++ b/spec/ruby/core/io/closed_spec.rb @@ -0,0 +1,20 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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/codepoints_spec.rb b/spec/ruby/core/io/codepoints_spec.rb new file mode 100644 index 0000000000..6e6b9613b5 --- /dev/null +++ b/spec/ruby/core/io/codepoints_spec.rb @@ -0,0 +1,25 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/codepoints', __FILE__) + +# See redmine #1667 +describe "IO#codepoints" do + it_behaves_like(:io_codepoints, :codepoints) +end + +describe "IO#codepoints" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "calls the given block" do + r = [] + @io.codepoints { |c| r << c } + r[24].should == 232 + r.last.should == 10 + 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..0a2ea70560 --- /dev/null +++ b/spec/ruby/core/io/constants_spec.rb @@ -0,0 +1,19 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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..344746fac3 --- /dev/null +++ b/spec/ruby/core/io/copy_stream_spec.rb @@ -0,0 +1,282 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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) + + lambda { 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.close + @to_io = new_io @to_name, "r" + lambda { IO.copy_stream @object.from, @to_io }.should raise_error(IOError) + 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, @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" + lambda { 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 + 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) + + lambda { 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 + lambda { 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 +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..a90b04aa5d --- /dev/null +++ b/spec/ruby/core/io/dup_spec.rb @@ -0,0 +1,69 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#dup" do + before :each do + @file = tmp("rubinius_spec_io_dup_#{$$}_#{Time.now.to_f}") + @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 + lambda { @f.gets }.should_not raise_error(Exception) + + @i.closed?.should == true + @f.closed?.should == false + end + + it "allows closing the original IO without affecting the new one" do + @f.close + lambda { @i.gets }.should_not raise_error(Exception) + + @i.closed?.should == false + @f.closed?.should == true + end + + it "raises IOError on closed stream" do + lambda { IOSpecs.closed_io.dup }.should raise_error(IOError) + 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..0dc535a159 --- /dev/null +++ b/spec/ruby/core/io/each_byte_spec.rb @@ -0,0 +1,57 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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..69c6739920 --- /dev/null +++ b/spec/ruby/core/io/each_char_spec.rb @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/chars', __FILE__) + +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..eb16e004fa --- /dev/null +++ b/spec/ruby/core/io/each_codepoint_spec.rb @@ -0,0 +1,45 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/codepoints', __FILE__) + +# See redmine #1667 +describe "IO#each_codepoint" do + it_behaves_like(:io_codepoints, :codepoints) +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 + + ruby_version_is "2.3" do # earlier versions stay blocked + it "raises an exception at incomplete character before EOF when conversion takes place" do + lambda { @io.each_codepoint {} }.should raise_error(ArgumentError) + end + 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..d4d8af7902 --- /dev/null +++ b/spec/ruby/core/io/each_line_spec.rb @@ -0,0 +1,11 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/each', __FILE__) + +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..4d6aa50fb2 --- /dev/null +++ b/spec/ruby/core/io/each_spec.rb @@ -0,0 +1,11 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/each', __FILE__) + +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..a2c5e563f0 --- /dev/null +++ b/spec/ruby/core/io/eof_spec.rb @@ -0,0 +1,107 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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.eof?.should == true } + end + + it "raises IOError on stream not opened for reading" do + lambda 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.eof?.should == false + end + + it "returns true after reading with read with no parameters" do + @io.read() + @io.eof?.should == true + end + + it "returns true after reading with read" do + @io.read(File.size(@name)) + @io.eof?.should == true + end + + it "returns true after reading with sysread" do + @io.sysread(File.size(@name)) + @io.eof?.should == true + end + + it "returns true after reading with readlines" do + @io.readlines + @io.eof?.should == true + end + + it "returns false on just opened non-empty stream" do + @io.eof?.should == false + end + + it "does not consume the data from the stream" do + @io.eof?.should == false + @io.getc.should == 'V' + end + + it "raises IOError on closed stream" do + lambda { IOSpecs.closed_io.eof? }.should raise_error(IOError) + end + + it "raises IOError on stream closed for reading by close_read" do + @io.close_read + lambda { @io.eof? }.should raise_error(IOError) + end + + it "returns true on one-byte stream after single-byte read" do + File.open(File.dirname(__FILE__) + '/fixtures/one_byte.txt') { |one_byte| + one_byte.read(1) + one_byte.eof?.should == true + } + 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.eof?.should == true + end + + it "returns false on receiving side of Pipe when writing side wrote some data" do + @r, @w = IO.pipe + @w.puts "hello" + @r.eof?.should == false + @w.close + @r.eof?.should == false + @r.read + @r.eof?.should == true + 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..ec85eba2ee --- /dev/null +++ b/spec/ruby/core/io/external_encoding_spec.rb @@ -0,0 +1,218 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +with_feature :encoding do + 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 + + 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::ASCII_8BIT" do + @io = new_io @name, "rb" + @io.external_encoding.should equal(Encoding::ASCII_8BIT) + 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::ASCII_8BIT" do + @io = new_io @name, "wb" + @io.external_encoding.should equal(Encoding::ASCII_8BIT) + 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 +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..0e20f50f60 --- /dev/null +++ b/spec/ruby/core/io/fcntl_spec.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#fcntl" do + it "raises IOError on closed stream" do + lambda { 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..9bcbbda336 --- /dev/null +++ b/spec/ruby/core/io/fdatasync_spec.rb @@ -0,0 +1,5 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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..259ac38073 --- /dev/null +++ b/spec/ruby/core/io/fileno_spec.rb @@ -0,0 +1,12 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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..fb431d5023 --- /dev/null +++ b/spec/ruby/core/io/fixtures/classes.rb @@ -0,0 +1,177 @@ +# -*- encoding: utf-8 -*- + +module IOSpecs + class SubIO < IO + 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_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_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 + + # Creates an IO instance for an existing fixture file. The + # file should obviously not be deleted. + def self.io_fixture(name, options_or_mode="r:utf-8") + path = fixture __FILE__, name + name = path if File.exist? path + new_io name, options_or_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=nil) + @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=nil) + @io.readpartial size, buf + end + + def send(*args) + raise "send called" + end + end +end 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..c877650ecd --- /dev/null +++ b/spec/ruby/core/io/flush_spec.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#flush" do + it "raises IOError on closed stream" do + lambda { IOSpecs.closed_io.flush }.should raise_error(IOError) + 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..b4abc1f87c --- /dev/null +++ b/spec/ruby/core/io/for_fd_spec.rb @@ -0,0 +1,10 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/new', __FILE__) + +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..f5fa1459e9 --- /dev/null +++ b/spec/ruby/core/io/foreach_spec.rb @@ -0,0 +1,81 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/readlines', __FILE__) + +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 + + 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 + IO.foreach(cmd) { |l| ScratchPad << l } + ScratchPad.recorded.should == ["hello\n", "line2\n"] + end + + with_feature :fork do + it "gets data from a fork when passed -" do + parent_pid = $$ + + IO.foreach("|-") { |l| ScratchPad << l } + + 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 +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..7816ecc42b --- /dev/null +++ b/spec/ruby/core/io/fsync_spec.rb @@ -0,0 +1,24 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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..cb8929890f --- /dev/null +++ b/spec/ruby/core/io/getbyte_spec.rb @@ -0,0 +1,42 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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 diff --git a/spec/ruby/core/io/getc_spec.rb b/spec/ruby/core/io/getc_spec.rb new file mode 100644 index 0000000000..dfff1a9583 --- /dev/null +++ b/spec/ruby/core/io/getc_spec.rb @@ -0,0 +1,42 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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..d984e795c2 --- /dev/null +++ b/spec/ruby/core/io/gets_spec.rb @@ -0,0 +1,313 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/gets_ascii', __FILE__) + +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 "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 + lambda { 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 "returns tainted strings" do + while line = @io.gets + line.tainted?.should == true + end + 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 "returns tainted strings" do + while line = @io.gets(nil) + line.tainted?.should == true + end + 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 "returns tainted strings" do + while line = @io.gets("") + line.tainted?.should == true + end + 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 "returns tainted strings" do + while line = @io.gets("la") + line.tainted?.should == true + end + 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 + 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 + lambda { File.open(@name, fmode("a:utf-8")) { |f| f.gets } }.should raise_error(IOError) + end + + it "raises an IOError if the stream is opened for writing only" do + lambda { File.open(@name, fmode("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, fmode("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 seperator 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 +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, fmode("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::ASCII_8BIT + 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 ASCII-8BIT" do + Encoding.default_external = Encoding::ASCII_8BIT + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.gets.encoding.should == Encoding::ASCII_8BIT + end + + it "transcodes to internal encoding if the IO object's external encoding is ASCII-8BIT" do + Encoding.default_external = Encoding::ASCII_8BIT + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.set_encoding Encoding::ASCII_8BIT, Encoding::UTF_8 + @io.gets.encoding.should == Encoding::UTF_8 + 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..4731257625 --- /dev/null +++ b/spec/ruby/core/io/initialize_spec.rb @@ -0,0 +1,53 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#initialize" do + before :each do + @name = tmp("io_initialize.txt") + @io = new_io @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 a Fixnum" do + fd = new_fd @name, "r:utf-8" + @io.send :initialize, fd, 'r' + @io.fileno.should == fd + # initialize has closed the old descriptor + lambda { IO.for_fd(@fd).close }.should raise_error(Errno::EBADF) + 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 + # initialize has closed the old descriptor + lambda { IO.for_fd(@fd).close }.should raise_error(Errno::EBADF) + end + + it "raises a TypeError when passed an IO" do + lambda { @io.send :initialize, STDOUT, 'w' }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + lambda { @io.send :initialize, nil, 'w' }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + lambda { @io.send :initialize, "4", 'w' }.should raise_error(TypeError) + end + + it "raises IOError on closed stream" do + lambda { @io.send :initialize, IOSpecs.closed_io.fileno }.should raise_error(IOError) + end + + it "raises an Errno::EBADF when given an invalid file descriptor" do + lambda { @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..1c6bf99b44 --- /dev/null +++ b/spec/ruby/core/io/inspect_spec.rb @@ -0,0 +1,23 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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..7578559838 --- /dev/null +++ b/spec/ruby/core/io/internal_encoding_spec.rb @@ -0,0 +1,140 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +with_feature :encoding do + 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 ASCII-8BIT and the internal encoding is not set" do + Encoding.default_external = Encoding::ASCII_8BIT + @io = new_io @name, @object + @io.internal_encoding.should be_nil + end + + it "returns nil when the external encoding is ASCII-8BIT and the internal encoding is not set" do + @io = new_io @name, "#{@object}:ascii-8bit" + @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 + + 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 +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..e196628ef0 --- /dev/null +++ b/spec/ruby/core/io/io_spec.rb @@ -0,0 +1,11 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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..3b2d271c0c --- /dev/null +++ b/spec/ruby/core/io/ioctl_spec.rb @@ -0,0 +1,32 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#ioctl" do + platform_is_not :windows do + it "raises IOError on closed stream" do + lambda { IOSpecs.closed_io.ioctl(5, 5) }.should raise_error(IOError) + end + end + + platform_is :linux do + platform_is "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 an Errno error when ioctl fails" do + File.open(__FILE__, 'r') do |f| + lambda { + # TIOCGWINSZ in /usr/include/asm-generic/ioctls.h + f.ioctl 0x5413, nil + }.should raise_error(Errno::ENOTTY) + 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..87172dddd7 --- /dev/null +++ b/spec/ruby/core/io/isatty_spec.rb @@ -0,0 +1,6 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/tty', __FILE__) + +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..4fddbf135c --- /dev/null +++ b/spec/ruby/core/io/lineno_spec.rb @@ -0,0 +1,95 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { IOSpecs.closed_io.lineno }.should raise_error(IOError) + 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 + lambda { IOSpecs.closed_io.lineno = 5 }.should raise_error(IOError) + 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 on nil argument" do + lambda { @io.lineno = nil }.should raise_error(TypeError) + 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/lines_spec.rb b/spec/ruby/core/io/lines_spec.rb new file mode 100644 index 0000000000..90ddc4e4cf --- /dev/null +++ b/spec/ruby/core/io/lines_spec.rb @@ -0,0 +1,42 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#lines" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "returns an Enumerator" do + @io.lines.should be_an_instance_of(Enumerator) + end + + describe "when no block is given" do + it "returns an Enumerator" do + @io.lines.should be_an_instance_of(Enumerator) + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.lines.size.should == nil + end + end + end + end + + it "returns a line when accessed" do + enum = @io.lines + enum.first.should == IOSpecs.lines[0] + end + + it "yields each line to the passed block" do + ScratchPad.record [] + @io.lines { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines + 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..ce922a4856 --- /dev/null +++ b/spec/ruby/core/io/new_spec.rb @@ -0,0 +1,10 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/new', __FILE__) + +describe "IO.new" do + it_behaves_like :io_new, :new +end + +describe "IO.new" do + it_behaves_like :io_new_errors, :new +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..f87ee6c7c2 --- /dev/null +++ b/spec/ruby/core/io/open_spec.rb @@ -0,0 +1,86 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/new', __FILE__) + +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 "propagates an exception raised by #close that is not a StandardError" do + lambda 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 + lambda 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 a 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 a 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..2d52315430 --- /dev/null +++ b/spec/ruby/core/io/output_spec.rb @@ -0,0 +1,27 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#<<" do + it "writes an object to the IO stream" do + lambda { + $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 + lambda { + $stderr << 1337 + }.should output_to_fd("1337", $stderr) + end + + it "raises an error if the stream is closed" do + io = IOSpecs.closed_io + lambda { io << "test" }.should raise_error(IOError) + end + + it "returns self" do + lambda { + ($stderr << "to_stderr").should == $stderr + }.should output(nil, "to_stderr") + 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..a4f6bbfcf8 --- /dev/null +++ b/spec/ruby/core/io/pid_spec.rb @@ -0,0 +1,35 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes.rb', __FILE__) + +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 + lambda { @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..b4984fdeb7 --- /dev/null +++ b/spec/ruby/core/io/pipe_spec.rb @@ -0,0 +1,214 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 +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.closed?.should == true + w.closed?.should == true + end + + it "closes both IO objects when the block raises" do + r = w = nil + lambda do + IO.pipe do |_r, _w| + r = _r + w = _w + raise RuntimeError + end + end.should raise_error(RuntimeError) + r.closed?.should == true + w.closed?.should == true + 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.closed?.should == true + w.closed?.should == true + 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..45bb0e2cab --- /dev/null +++ b/spec/ruby/core/io/popen_spec.rb @@ -0,0 +1,285 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO.popen" do + before :each do + @io = nil + end + + after :each do + @io.close if @io + 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(ruby_cmd('puts "foo"'), "r") + @io.read.should == "foo\n" + end + + it "raises IOError when writing a read-only pipe" do + @io = IO.popen(ruby_cmd('puts "foo"'), "r") + lambda { @io.write('foo') }.should raise_error(IOError) + end +end + +describe "IO.popen" do + before :each do + @fname = tmp("IO_popen_spec") + @io = nil + end + + after :each do + @io.close if @io and !@io.closed? + rm_r @fname + 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") + lambda { @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 + $?.signaled?.should == true + end + platform_is :windows do + $?.exited?.should == true + 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 +end + +describe "IO.popen" do + before :each do + @io = nil + end + + after :each do + @io.close if @io + 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 + + with_feature :fork 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 + + with_feature :encoding do + 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 + end + + context "with a leading ENV Hash" do + it "accepts a single String command" do + IO.popen({"FOO" => "bar"}, ruby_cmd('puts ENV["FOO"]')) do |io| + io.read.should == "bar\n" + end + end + + it "accepts a single String command, and an IO mode" do + IO.popen({"FOO" => "bar"}, ruby_cmd('puts ENV["FOO"]'), "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..300925a284 --- /dev/null +++ b/spec/ruby/core/io/pos_spec.rb @@ -0,0 +1,12 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/pos', __FILE__) + +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/print_spec.rb b/spec/ruby/core/io/print_spec.rb new file mode 100644 index 0000000000..e852757385 --- /dev/null +++ b/spec/ruby/core/io/print_spec.rb @@ -0,0 +1,54 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe IO, "#print" do + before :each do + @old_separator = $\ + $\ = '->' + @name = tmp("io_print") + end + + after :each do + $\ = @old_separator + rm_r @name + 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 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 + + 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 + lambda { 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..34be4c8e79 --- /dev/null +++ b/spec/ruby/core/io/printf_spec.rb @@ -0,0 +1,32 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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..9803d0c1a1 --- /dev/null +++ b/spec/ruby/core/io/putc_spec.rb @@ -0,0 +1,11 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../../../shared/io/putc', __FILE__) + +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..8f2f4f2c81 --- /dev/null +++ b/spec/ruby/core/io/puts_spec.rb @@ -0,0 +1,144 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + $/ = @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 + lambda { $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 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.should_receive(:write).with("#<Object:0x...>") + @io.should_receive(:write).with("\n") + @io.puts(object).should == nil + end + + it "calls :to_ary before writing non-string objects" do + object = mock('hola') + object.should_receive(:to_ary).and_return(["hola"]) + + @io.should_receive(:write).with("hola") + @io.should_receive(:write).with("\n") + @io.puts(object).should == nil + 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.should_receive(:write).with(object.inspect.split(" ")[0] + ">") + @io.should_receive(:write).with("\n") + @io.puts(object).should == nil + 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 + $/ = ":" + @io.puts(5).should == nil + ScratchPad.recorded.should == "5\n" + end + + it "raises IOError on closed stream" do + lambda { IOSpecs.closed_io.puts("stuff") }.should raise_error(IOError) + end + + with_feature :encoding do + 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 +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..2b314987bc --- /dev/null +++ b/spec/ruby/core/io/read_nonblock_spec.rb @@ -0,0 +1,85 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { @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 + + ruby_version_is "2.3" do + 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 + end + + it "returns at most the number of bytes requested" do + @write << "hello" + @read.read_nonblock(4).should == "hell" + 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 "reads into the passed buffer" do + buffer = "" + @write.write("1") + @read.read_nonblock(1, buffer) + buffer.should == "1" + end + + it "raises IOError on closed stream" do + lambda { 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) + + lambda { @read.read_nonblock(5) }.should raise_error(EOFError) + 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..223e3cde06 --- /dev/null +++ b/spec/ruby/core/io/read_spec.rb @@ -0,0 +1,616 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + + it "accepts an empty options Hash" do + IO.read(@fname, {}).should == @contents + 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 + lambda { 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 + lambda { 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 + + 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 "raises an Errno::ENOENT when the requested file does not exist" do + rm_r @fname + lambda { IO.read @fname }.should raise_error(Errno::ENOENT) + end + + it "raises a TypeError when not passed a String type" do + lambda { IO.read nil }.should raise_error(TypeError) + end + + it "raises an ArgumentError when not passed a valid length" do + lambda { IO.read @fname, -1 }.should raise_error(ArgumentError) + end + + it "raises an Errno::EINVAL when not passed a valid offset" do + lambda { IO.read @fname, 0, -1 }.should raise_error(Errno::EINVAL) + lambda { IO.read @fname, -1, -1 }.should raise_error(Errno::EINVAL) + end + + with_feature :encoding do + 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 + end +end + +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 + IO.read(cmd).should == "hello\n" + end + + with_feature :fork do + it "opens a pipe to a fork if the rest is -" do + str = IO.read("|-") + 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 + IO.read(cmd, 1).should == "h" + end + + platform_is_not :windows do + it "raises Errno::ESPIPE if passed an offset" do + lambda { + IO.read("|sh -c 'echo hello'", 1, 1) + }.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 + lambda { + IO.read("|cmd.exe /C echo hello", 1, 1) + }.should raise_error(Errno::EINVAL) + 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 "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 == '' + end + + it "consumes zero bytes when reading zero bytes" do + @io.read(0).should == '' + @io.pos.should == 0 + + @io.getc.chr.should == '1' + end + + it "is at end-of-file when everything has been read" do + @io.read + @io.eof?.should == true + 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 "returns the given buffer" do + buf = "" + + @io.read(nil, buf).object_id.should == buf.object_id + 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).object_id.should_not == obj.object_id + 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 + lambda { IOSpecs.closed_io.read }.should raise_error(IOError) + 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::ASCII_8BIT + xE2 = [226].pack('C*') + result.should == ("abc" + xE2 + "def").force_encoding(Encoding::ASCII_8BIT) + end + + it "does not transcode file contents when an internal encoding is specified" do + result = File.open(@name, "r:binary:utf-8") { |f| f.read }.chomp + result.encoding.should == Encoding::ASCII_8BIT + xE2 = [226].pack('C*') + result.should == ("abc" + xE2 + "def").force_encoding(Encoding::ASCII_8BIT) + 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("ascii-8bit").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("ascii-8bit").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("ascii-8bit").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("ascii-8bit").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("ascii-8bit").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("ascii-8bit").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 + +with_feature :encoding do + 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 = "".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::ASCII_8BIT) + end + + it "returns a String in ASCII-8BIT when passed a size" do + @io.read(4).encoding.should equal(Encoding::ASCII_8BIT) + end + + it "does not change the buffer's encoding when passed a limit" do + buf = "".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 "trucates the buffer but does not change the buffer's encoding when no data remains" do + buf = "abc".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 +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..1cc588eea5 --- /dev/null +++ b/spec/ruby/core/io/readbyte_spec.rb @@ -0,0 +1,26 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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) + lambda do + @io.readbyte + end.should raise_error EOFError + end + + it "needs to be reviewed for spec completeness" +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..6771fcab59 --- /dev/null +++ b/spec/ruby/core/io/readchar_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { @io.readchar }.should raise_error(EOFError) + end + + it "raises IOError on closed stream" do + lambda { IOSpecs.closed_io.readchar }.should raise_error(IOError) + 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 + lambda { @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..39706948eb --- /dev/null +++ b/spec/ruby/core/io/readline_spec.rb @@ -0,0 +1,45 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 } + lambda { @io.readline }.should raise_error(EOFError) + end + + it "raises IOError on closed stream" do + lambda { 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 +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..22ba844b52 --- /dev/null +++ b/spec/ruby/core/io/readlines_spec.rb @@ -0,0 +1,210 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/readlines', __FILE__) + +describe "IO#readlines" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @orig_exteenc = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + end + + after :each do + @io.close unless @io.closed? + Encoding.default_external = @orig_exteenc + end + + it "raises an IOError if the stream is closed" do + @io.close + lambda { @io.readlines }.should raise_error(IOError) + end + + describe "when passed no arguments" do + before :each do + @sep, $/ = $/, " " + end + + after :each do + $/ = @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 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 = IO.readlines(cmd) + lines.should == ["hello\n", "line2\n"] + end + + with_feature :fork do + it "gets data from a fork when passed -" do + lines = IO.readlines("|-") + + if lines # parent + lines.should == ["hello\n", "from a fork\n"] + else + puts "hello" + puts "from a fork" + exit! + end + end + 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 + lambda do + File.open(@name, fmode("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 + lambda do + File.open(@name, fmode("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 + + 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 + $/ = @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 + $/ = $/.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 ASCII-8BIT" do + Encoding.default_external = Encoding::ASCII_8BIT + Encoding.default_internal = Encoding::UTF_8 + lines = IO.readlines(@name) + lines.all? { |s| s.encoding == Encoding::ASCII_8BIT }.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..16cb08dada --- /dev/null +++ b/spec/ruby/core/io/readpartial_spec.rb @@ -0,0 +1,96 @@ +# -*- encoding: ascii-8bit -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { IOSpecs.closed_io.readpartial(10) }.should raise_error(IOError) + + @rd.close + lambda { @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" + @wr.write("hello world") + @wr.close + @rd.readpartial(11, buffer) + buffer.should == "hello world" + end + + it "raises EOFError on EOF" do + @wr.write("abc") + @wr.close + @rd.readpartial(10).should == 'abc' + lambda { @rd.readpartial(10) }.should raise_error(EOFError) + end + + it "discards the existing buffer content upon error" do + buffer = 'hello' + @wr.close + lambda { @rd.readpartial(1, buffer) }.should raise_error(EOFError) + buffer.should be_empty + end + + it "raises IOError if the stream is closed" do + @wr.close + lambda { @rd.readpartial(1) }.should raise_error(IOError) + end + + it "raises ArgumentError if the negative argument is provided" do + lambda { @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 + +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..63020d5f3e --- /dev/null +++ b/spec/ruby/core/io/reopen_spec.rb @@ -0,0 +1,302 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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) + lambda { @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") + lambda { @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) + lambda { @io.reopen(STDOUT) }.should raise_error(IOError) + end + + it "raises an IOError if the IO argument is closed" do + lambda { @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 + lambda { @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 "closes the file descriptor obtained by opening the new file" do + @io = new_io @name, "w" + + @other_io = File.open @other_name, "w" + max = @other_io.fileno + @other_io.close + + @io.reopen @other_name + + @other_io = File.open @other_name, "w" + @other_io.fileno.should == max + 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.exist?(@other_name).should be_true + 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.exist?(@other_name).should be_true + 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" + lambda { @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 = new_io @other_name, "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 "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..ecf8a71891 --- /dev/null +++ b/spec/ruby/core/io/rewind_spec.rb @@ -0,0 +1,38 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 input and clears EOF" do + value = @io.read + @io.rewind + @io.eof?.should == false + 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 "raises IOError on closed stream" do + lambda { 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..f7e138cbe9 --- /dev/null +++ b/spec/ruby/core/io/seek_spec.rb @@ -0,0 +1,79 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/pos', __FILE__) + +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 + lambda { @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.eof?.should == false + 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.eof?.should == false + 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.eof?.should == false + 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..aa1199c03b --- /dev/null +++ b/spec/ruby/core/io/select_spec.rb @@ -0,0 +1,115 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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.write("be ready") + result = IO.select [@rd], [@wr], nil, 0 + result.should == [[@rd], [@wr], []] + 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 even when monitoring the same object in different arrays" do + filename = tmp("IO_select_pipe_file") + $$.to_s + io = File.open(filename, 'w+') + result = IO.select [io], [io], nil, 0 + result.should == [[io], [io], []] + io.close + rm_r filename + 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], [], []] + + obj = mock("write_io") + obj.should_receive(:to_io).at_least(1).and_return(@wr) + IO.select(nil, [obj]).should == [[], [obj], []] + end + + it "raises TypeError if supplied objects are not IO" do + lambda { IO.select([Object.new]) }.should raise_error(TypeError) + lambda { IO.select(nil, [Object.new]) }.should raise_error(TypeError) + + obj = mock("io") + obj.should_receive(:to_io).any_number_of_times.and_return(nil) + + lambda { IO.select([obj]) }.should raise_error(TypeError) + lambda { IO.select(nil, [obj]) }.should raise_error(TypeError) + end + + it "raises a TypeError if the specified timeout value is not Numeric" do + lambda { IO.select([@rd], nil, nil, Object.new) }.should raise_error(TypeError) + end + + it "raises TypeError if the first three arguments are not Arrays" do + lambda { IO.select(Object.new)}.should raise_error(TypeError) + lambda { IO.select(nil, Object.new)}.should raise_error(TypeError) + lambda { IO.select(nil, nil, Object.new)}.should raise_error(TypeError) + end + + it "raises an ArgumentError when passed a negative timeout" do + lambda { IO.select(nil, nil, nil, -5)}.should raise_error(ArgumentError) + 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.status.should == "sleep" + t.kill + t.join + 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..1d6e2a8f3b --- /dev/null +++ b/spec/ruby/core/io/set_encoding_spec.rb @@ -0,0 +1,193 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +with_feature :encoding do + describe :io_set_encoding_write, shared: true do + it "sets the encodings to nil" 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 "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 + + 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::ASCII_8BIT) + + @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 + 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 separanted 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 + 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..67f0fd5c86 --- /dev/null +++ b/spec/ruby/core/io/shared/binwrite.rb @@ -0,0 +1,78 @@ +require File.expand_path('../../fixtures/classes', __FILE__) + +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 "creates a file if missing" do + fn = @filename + "xxx" + begin + File.exist?(fn).should be_false + IO.send(@method, fn, "test") + File.exist?(fn).should be_true + ensure + rm_r fn + end + end + + it "creates file if missing even if offset given" do + fn = @filename + "xxx" + begin + File.exist?(fn).should be_false + IO.send(@method, fn, "test", 0) + File.exist?(fn).should be_true + 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 "raises an error if readonly mode is specified" do + lambda { 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..7f2edd2b6d --- /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 + lambda { IOSpecs.closed_io.send(@method).first }.should raise_error(IOError) + end + + it "raises IOError on closed stream" do + lambda { 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..3bb3dce939 --- /dev/null +++ b/spec/ruby/core/io/shared/codepoints.rb @@ -0,0 +1,54 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../fixtures/classes', __FILE__) + +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 + lambda { @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 + lambda { 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..dc07434ecd --- /dev/null +++ b/spec/ruby/core/io/shared/each.rb @@ -0,0 +1,135 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../fixtures/classes', __FILE__) + +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 "returns self" do + @io.send(@method) { |l| l }.should equal(@io) + end + + it "raises an IOError when self is not readable" do + lambda { 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 + lambda { @io.send(@method, 0){} }.should raise_error(ArgumentError) + end + 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 + end +end + +describe :io_each_default_separator, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + @sep, $/ = $/, " " + end + + after :each do + @io.close if @io + $/ = @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..2a8fe3c9a5 --- /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..12f889f646 --- /dev/null +++ b/spec/ruby/core/io/shared/new.rb @@ -0,0 +1,378 @@ +require File.expand_path('../../fixtures/classes', __FILE__) + +# 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 a Fixnum 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 + verbose, $VERBOSE = $VERBOSE, nil + stdout = STDOUT + stdout_file = tmp("stdout.txt") + + begin + @io = IO.send(@method, @fd, "w") + @io.should be_an_instance_of(IO) + ensure + STDOUT = stdout + $VERBOSE = verbose + rm_r stdout_file + end + end + + it "creates an IO instance when STDERR is closed" do + verbose, $VERBOSE = $VERBOSE, nil + 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 + $VERBOSE = verbose + rm_r stderr_file + end + end + + it "calls #to_int on an object to convert to a Fixnum" 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 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 + lambda { + @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 + lambda { + @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.binmode?.should == true + end + + it "does not set binmode without being asked" do + @io = IO.send(@method, @fd, 'w') + @io.binmode?.should == false + end + + it "sets binmode from :binmode option" do + @io = IO.send(@method, @fd, 'w', {binmode: true}) + @io.binmode?.should == true + end + + it "does not set binmode from false :binmode" do + @io = IO.send(@method, @fd, 'w', {binmode: false}) + @io.binmode?.should == false + end + + it "sets external encoding to binary with binmode in mode string" do + @io = IO.send(@method, @fd, 'wb') + @io.external_encoding.to_s.should == 'ASCII-8BIT' + end + + # #5917 + it "sets external encoding to binary with :binmode option" do + @io = IO.send(@method, @fd, 'w', {binmode: true}) + @io.external_encoding.to_s.should == 'ASCII-8BIT' + 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 "accepts nil options" do + @io = IO.send(@method, @fd, 'w', nil) + @io.write("foo").should == 3 + 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.autoclose?.should == false + @io.autoclose = true + end + + it "accepts any truthy option :autoclose" do + @io = IO.send(@method, @fd, 'w', autoclose: 42) + @io.autoclose?.should == true + 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 + lambda { IO.send(@method, -1, "w") }.should raise_error(Errno::EBADF) + end + + it "raises an IOError if passed a closed stream" do + lambda { 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 + lambda { IO.send(@method, @fd, "r") }.should raise_error(Errno::EINVAL) + end + end + + it "raises ArgumentError if passed an empty mode string" do + lambda { IO.send(@method, @fd, "") }.should raise_error(ArgumentError) + end + + it "raises an error if passed modes two ways" do + lambda { + IO.send(@method, @fd, "w", mode: "w") + }.should raise_error(ArgumentError) + end + + it "raises an error if passed encodings two ways" do + lambda { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', {encoding: 'ISO-8859-1'}) + }.should raise_error(ArgumentError) + lambda { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', {external_encoding: 'ISO-8859-1'}) + }.should raise_error(ArgumentError) + lambda { + @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 + lambda { + @io = IO.send(@method, @fd, "wb", binmode: true) + }.should raise_error(ArgumentError) + lambda { + @io = IO.send(@method, @fd, "wt", textmode: true) + }.should raise_error(ArgumentError) + + lambda { + @io = IO.send(@method, @fd, "wb", textmode: false) + }.should raise_error(ArgumentError) + lambda { + @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 + lambda { + @io = IO.send(@method, @fd, "wb", binmode: false) + }.should raise_error(ArgumentError) + lambda { + @io = IO.send(@method, @fd, "wt", textmode: false) + }.should raise_error(ArgumentError) + + lambda { + @io = IO.send(@method, @fd, "wb", textmode: true) + }.should raise_error(ArgumentError) + lambda { + @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 + lambda { + @io = IO.send(@method, @fd, "w", textmode: true, binmode: true) + }.should raise_error(ArgumentError) + lambda { + @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 + lambda { + @io = IO.send(@method, @fd, 'w', false) + }.should raise_error(ArgumentError) + lambda { + @io = IO.send(@method, @fd, false, false) + }.should raise_error(ArgumentError) + lambda { + @io = IO.send(@method, @fd, nil, false) + }.should raise_error(ArgumentError) + end + + it "raises TypeError if passed a hash for mode and nil for options" do + lambda { + @io = IO.send(@method, @fd, {mode: 'w'}, nil) + }.should raise_error(TypeError) + 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..fef7ab2bf7 --- /dev/null +++ b/spec/ruby/core/io/shared/pos.rb @@ -0,0 +1,72 @@ +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 + lambda { 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.eof?.should == false + 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 "does not accept Bignums that don't fit in a C long" do + File.open @fname do |io| + lambda { io.send @method, 2**128 }.should raise_error(RangeError) + end + end + + it "raises IOError on closed stream" do + lambda { 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..4cb821274a --- /dev/null +++ b/spec/ruby/core/io/shared/readlines.rb @@ -0,0 +1,204 @@ +describe :io_readlines, shared: true do + it "raises TypeError if the first parameter is nil" do + lambda { 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") + lambda { 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 +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 a Fixnum" do + before :each do + @sep = $/ + end + + after :each do + $/ = @sep + end + + it "defaults to $/ as the separator" do + $/ = " " + 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 a Fixnum" do + result = IO.send(@method, @name, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_limit + 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".force_encoding("utf-8"), &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_arbitrary_separator + end + end + + describe "when the object is a Hash" do + it "uses the value as the options hash" do + result = IO.send(@method, @name, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end + end + end + + describe "when passed name, object, object" do + describe "when the first object is a Fixnum" do + it "uses the second object as an options Hash" do + lambda do + IO.send(@method, @filename, 10, mode: "w", &@object) + end.should raise_error(IOError) + end + + it "calls #to_hash to convert the second object to a Hash" do + options = mock("io readlines options Hash") + options.should_receive(:to_hash).and_return({ mode: "w" }) + lambda do + IO.send(@method, @filename, 10, options, &@object) + end.should raise_error(IOError) + end + end + + describe "when the first object is a String" do + it "uses the second object as a limit if it is a Fixnum" 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 + + it "uses the second object as an options Hash" do + lambda do + IO.send(@method, @filename, " ", mode: "w", &@object) + end.should raise_error(IOError) + end + + it "calls #to_hash to convert the second object to a Hash" do + options = mock("io readlines options Hash") + options.should_receive(:to_hash).and_return({ mode: "w" }) + lambda do + IO.send(@method, @filename, " ", options, &@object) + end.should raise_error(IOError) + end + end + + describe "when the first object is not a String or Fixnum" 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, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "uses the second object as a limit if it is a Fixnum" do + result = IO.send(@method, @name, " ", 10, mode: "r", &@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 + + it "uses the second object as an options Hash" do + lambda do + IO.send(@method, @filename, " ", mode: "w", &@object) + end.should raise_error(IOError) + end + + it "calls #to_hash to convert the second object to a Hash" do + options = mock("io readlines options Hash") + options.should_receive(:to_hash).and_return({ mode: "w" }) + lambda do + IO.send(@method, @filename, " ", options, &@object) + end.should raise_error(IOError) + end + end + end + + describe "when passed name, separator, limit, options" 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 "calls #to_hash to convert the options object" do + options = mock("io readlines options Hash") + options.should_receive(:to_hash).and_return({ mode: "w" }) + lambda do + IO.send(@method, @filename, " ", 10, options, &@object) + end.should raise_error(IOError) + 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..eddc5d15af --- /dev/null +++ b/spec/ruby/core/io/shared/tty.rb @@ -0,0 +1,25 @@ +require File.expand_path('../../fixtures/classes', __FILE__) + +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 + # workaround for not configured environment like OS X + 1.should == 1 + 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 + lambda { 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..fd4b0af30e --- /dev/null +++ b/spec/ruby/core/io/shared/write.rb @@ -0,0 +1,72 @@ +# encoding: utf-8 +require File.expand_path('../../fixtures/classes', __FILE__) + +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 + lambda { @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) + lambda { @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 + lambda { IOSpecs.closed_io.send(@method, "hello") }.should raise_error(IOError) + 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..d59535843a --- /dev/null +++ b/spec/ruby/core/io/stat_spec.rb @@ -0,0 +1,24 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "IO#stat" do + before :each do + @io = IO.popen 'cat', "r+" + end + + after :each do + @io.close unless @io.closed? + end + + it "raises IOError on closed stream" do + lambda { 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..5cd873d799 --- /dev/null +++ b/spec/ruby/core/io/sync_spec.rb @@ -0,0 +1,64 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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 + lambda { 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..f6d37de364 --- /dev/null +++ b/spec/ruby/core/io/sysopen_spec.rb @@ -0,0 +1,50 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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(Fixnum) + @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(Fixnum) + @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 + lambda { @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..1993653df9 --- /dev/null +++ b/spec/ruby/core/io/sysread_spec.rb @@ -0,0 +1,82 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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") + 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 "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") + lambda { @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") + lambda { @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 + lambda { IOSpecs.closed_io.sysread(5) }.should raise_error(IOError) + 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..bcce968c7d --- /dev/null +++ b/spec/ruby/core/io/sysseek_spec.rb @@ -0,0 +1,44 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/pos', __FILE__) + +describe "IO#sysseek" do + it_behaves_like :io_set_pos, :seek +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 + lambda { @io.sysseek(-5, IO::SEEK_CUR) }.should raise_error(IOError) + 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 + lambda { @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..879423de2e --- /dev/null +++ b/spec/ruby/core/io/syswrite_spec.rb @@ -0,0 +1,54 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/write', __FILE__) + +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 "warns if called immediately after a buffered IO#write" do + @file.write("abcde") + lambda { @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) + lambda { @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" do + it_behaves_like :io_write, :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..d2f523cf10 --- /dev/null +++ b/spec/ruby/core/io/tell_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/pos', __FILE__) + +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..bbe656cdcc --- /dev/null +++ b/spec/ruby/core/io/to_i_spec.rb @@ -0,0 +1,12 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { 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..76ebefb38f --- /dev/null +++ b/spec/ruby/core/io/to_io_spec.rb @@ -0,0 +1,21 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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..0326982ff1 --- /dev/null +++ b/spec/ruby/core/io/try_convert_spec.rb @@ -0,0 +1,49 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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") + lambda { IO.try_convert(obj) }.should raise_error(TypeError) + end + + it "propagates an exception raised by #to_io" do + obj = mock("io") + obj.should_receive(:to_io).and_raise(TypeError.new) + lambda{ 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..3c1449b030 --- /dev/null +++ b/spec/ruby/core/io/tty_spec.rb @@ -0,0 +1,6 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/tty', __FILE__) + +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..ee334b469b --- /dev/null +++ b/spec/ruby/core/io/ungetbyte_spec.rb @@ -0,0 +1,48 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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 "puts back one byte for an Integer argument" do + @io.ungetbyte(4095).should be_nil + @io.getbyte.should == 255 + end + + it "raises an IOError if the IO is closed" do + @io.close + lambda { @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..ce4cc9d346 --- /dev/null +++ b/spec/ruby/core/io/ungetc_spec.rb @@ -0,0 +1,119 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 "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.eof?.should == true + empty.getc.should == nil + empty.ungetc(100) + empty.eof?.should == false + } + 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) + lambda { @io.sysread(1) }.should raise_error(IOError) + end + + it "does not affect the stream and returns nil when passed nil" do + @io.getc.should == ?V + @io.ungetc(nil) + @io.getc.should == ?o + 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 closed stream" do + @io.getc + @io.close + lambda { @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..a6a263e931 --- /dev/null +++ b/spec/ruby/core/io/write_nonblock_spec.rb @@ -0,0 +1,76 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/write', __FILE__) + +# 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 "checks if the file is writable if writing zero bytes" do + lambda { @readonly_file.write_nonblock("") }.should raise_error + end + end + + describe "IO#write_nonblock" do + it_behaves_like :io_write, :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 + lambda { + 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 + + ruby_version_is "2.3" do + 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 } + 1.should == 1 + end + 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..1011efe8d5 --- /dev/null +++ b/spec/ruby/core/io/write_spec.rb @@ -0,0 +1,157 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/write', __FILE__) +require File.expand_path('../shared/binwrite', __FILE__) + +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 + lambda { @readonly_file.write("") }.should_not raise_error + end + + it "returns a length of 0 when writing a blank string" do + @file.write('').should == 0 + end + + with_feature :encoding do + 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 the number of bytes written" do + @file.write("hellø").should == 6 + 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 an :open_args option" do + IO.write(@filename, 'hi', open_args: ["w", nil, {encoding: Encoding::UTF_32LE}]).should == 8 + 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| + lambda { 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::ASCII_8BIT) + 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 "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 "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::ASCII_8BIT) + end + + platform_is_not :windows do + describe "on a FIFO" do + before :each do + @fifo = tmp("File_open_fifo") + system "mkfifo #{@fifo}" + end + + after :each do + rm_r @fifo + end + + 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 +end + +describe "IO#write" do + it_behaves_like :io_write, :write +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 |