summaryrefslogtreecommitdiff
path: root/spec/ruby/core/io
diff options
context:
space:
mode:
Diffstat (limited to 'spec/ruby/core/io')
-rw-r--r--spec/ruby/core/io/advise_spec.rb14
-rw-r--r--spec/ruby/core/io/binread_spec.rb10
-rw-r--r--spec/ruby/core/io/bytes_spec.rb47
-rw-r--r--spec/ruby/core/io/chars_spec.rb30
-rw-r--r--spec/ruby/core/io/close_read_spec.rb3
-rw-r--r--spec/ruby/core/io/close_spec.rb6
-rw-r--r--spec/ruby/core/io/close_write_spec.rb14
-rw-r--r--spec/ruby/core/io/codepoints_spec.rb38
-rw-r--r--spec/ruby/core/io/copy_stream_spec.rb33
-rw-r--r--spec/ruby/core/io/eof_spec.rb2
-rw-r--r--spec/ruby/core/io/fixtures/classes.rb26
-rw-r--r--spec/ruby/core/io/flush_spec.rb10
-rw-r--r--spec/ruby/core/io/foreach_spec.rb19
-rw-r--r--spec/ruby/core/io/getbyte_spec.rb16
-rw-r--r--spec/ruby/core/io/gets_spec.rb109
-rw-r--r--spec/ruby/core/io/initialize_spec.rb11
-rw-r--r--spec/ruby/core/io/ioctl_spec.rb2
-rw-r--r--spec/ruby/core/io/lineno_spec.rb15
-rw-r--r--spec/ruby/core/io/lines_spec.rb46
-rw-r--r--spec/ruby/core/io/new_spec.rb8
-rw-r--r--spec/ruby/core/io/nonblock_spec.rb46
-rw-r--r--spec/ruby/core/io/open_spec.rb13
-rw-r--r--spec/ruby/core/io/path_spec.rb14
-rw-r--r--spec/ruby/core/io/pipe_spec.rb11
-rw-r--r--spec/ruby/core/io/pread_spec.rb81
-rw-r--r--spec/ruby/core/io/print_spec.rb25
-rw-r--r--spec/ruby/core/io/puts_spec.rb2
-rw-r--r--spec/ruby/core/io/pwrite_spec.rb32
-rw-r--r--spec/ruby/core/io/read_nonblock_spec.rb53
-rw-r--r--spec/ruby/core/io/read_spec.rb180
-rw-r--r--spec/ruby/core/io/readchar_spec.rb66
-rw-r--r--spec/ruby/core/io/readline_spec.rb33
-rw-r--r--spec/ruby/core/io/readlines_spec.rb51
-rw-r--r--spec/ruby/core/io/readpartial_spec.rb19
-rw-r--r--spec/ruby/core/io/rewind_spec.rb15
-rw-r--r--spec/ruby/core/io/select_spec.rb48
-rw-r--r--spec/ruby/core/io/set_encoding_by_bom_spec.rb273
-rw-r--r--spec/ruby/core/io/set_encoding_spec.rb49
-rw-r--r--spec/ruby/core/io/shared/binwrite.rb13
-rw-r--r--spec/ruby/core/io/shared/each.rb86
-rw-r--r--spec/ruby/core/io/shared/new.rb50
-rw-r--r--spec/ruby/core/io/shared/pos.rb8
-rw-r--r--spec/ruby/core/io/shared/readlines.rb140
-rw-r--r--spec/ruby/core/io/shared/write.rb75
-rw-r--r--spec/ruby/core/io/stat_spec.rb3
-rw-r--r--spec/ruby/core/io/sysread_spec.rb39
-rw-r--r--spec/ruby/core/io/sysseek_spec.rb2
-rw-r--r--spec/ruby/core/io/syswrite_spec.rb11
-rw-r--r--spec/ruby/core/io/try_convert_spec.rb2
-rw-r--r--spec/ruby/core/io/ungetbyte_spec.rb18
-rw-r--r--spec/ruby/core/io/ungetc_spec.rb16
-rw-r--r--spec/ruby/core/io/write_nonblock_spec.rb11
-rw-r--r--spec/ruby/core/io/write_spec.rb157
53 files changed, 1592 insertions, 509 deletions
diff --git a/spec/ruby/core/io/advise_spec.rb b/spec/ruby/core/io/advise_spec.rb
index 0a845487e2..651fc52378 100644
--- a/spec/ruby/core/io/advise_spec.rb
+++ b/spec/ruby/core/io/advise_spec.rb
@@ -73,19 +73,9 @@ describe "IO#advise" do
end
end
- platform_is :linux do
+ guard -> { platform_is :linux and kernel_version_is '3.6' } do # [ruby-core:65355] tmpfs is not supported
it "supports the willneed advice type" do
- 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
- skip "[ruby-core:65355] tmpfs is not supported"
- else
- @io.advise(:willneed).should be_nil
- end
+ @io.advise(:willneed).should be_nil
end
end
diff --git a/spec/ruby/core/io/binread_spec.rb b/spec/ruby/core/io/binread_spec.rb
index a3f752d8f9..418e89213b 100644
--- a/spec/ruby/core/io/binread_spec.rb
+++ b/spec/ruby/core/io/binread_spec.rb
@@ -44,4 +44,14 @@ describe "IO.binread" do
it "raises an Errno::EINVAL when not passed a valid offset" do
-> { IO.binread @fname, 0, -1 }.should raise_error(Errno::EINVAL)
end
+
+ ruby_version_is "3.3" do
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ IO.binread(cmd)
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
end
diff --git a/spec/ruby/core/io/bytes_spec.rb b/spec/ruby/core/io/bytes_spec.rb
deleted file mode 100644
index 6e328983f2..0000000000
--- a/spec/ruby/core/io/bytes_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- encoding: utf-8 -*-
-require_relative '../../spec_helper'
-require_relative 'fixtures/classes'
-
-ruby_version_is ''...'3.0' do
- describe "IO#bytes" do
- before :each do
- @io = IOSpecs.io_fixture "lines.txt"
- @verbose, $VERBOSE = $VERBOSE, nil
- end
-
- after :each do
- $VERBOSE = @verbose
- @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
- -> { 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
- -> { enum.first }.should raise_error(IOError)
- end
- end
-end
diff --git a/spec/ruby/core/io/chars_spec.rb b/spec/ruby/core/io/chars_spec.rb
deleted file mode 100644
index 15db595aed..0000000000
--- a/spec/ruby/core/io/chars_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- encoding: utf-8 -*-
-require_relative '../../spec_helper'
-require_relative 'fixtures/classes'
-require_relative 'shared/chars'
-
-ruby_version_is ''...'3.0' do
- describe "IO#chars" do
- before :each do
- @verbose, $VERBOSE = $VERBOSE, nil
- end
-
- after :each do
- $VERBOSE = @verbose
- end
-
- it_behaves_like :io_chars, :chars
- end
-
- describe "IO#chars" do
- before :each do
- @verbose, $VERBOSE = $VERBOSE, nil
- end
-
- after :each do
- $VERBOSE = @verbose
- end
-
- it_behaves_like :io_chars_empty, :chars
- end
-end
diff --git a/spec/ruby/core/io/close_read_spec.rb b/spec/ruby/core/io/close_read_spec.rb
index 26454bfddd..e700e85bd9 100644
--- a/spec/ruby/core/io/close_read_spec.rb
+++ b/spec/ruby/core/io/close_read_spec.rb
@@ -4,7 +4,8 @@ require_relative 'fixtures/classes'
describe "IO#close_read" do
before :each do
- @io = IO.popen 'cat', "r+"
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ @io = IO.popen cmd, "r+"
@path = tmp('io.close.txt')
end
diff --git a/spec/ruby/core/io/close_spec.rb b/spec/ruby/core/io/close_spec.rb
index eb560eaf67..3a44cc8b17 100644
--- a/spec/ruby/core/io/close_spec.rb
+++ b/spec/ruby/core/io/close_spec.rb
@@ -44,6 +44,12 @@ describe "IO#close" do
@io.close.should be_nil
end
+ it "does not call the #flush method but flushes the stream internally" do
+ @io.should_not_receive(:flush)
+ @io.close
+ @io.should.closed?
+ end
+
it 'raises an IOError with a clear message' do
matching_exception = nil
diff --git a/spec/ruby/core/io/close_write_spec.rb b/spec/ruby/core/io/close_write_spec.rb
index 14835e4e2c..70610a3e9d 100644
--- a/spec/ruby/core/io/close_write_spec.rb
+++ b/spec/ruby/core/io/close_write_spec.rb
@@ -3,7 +3,8 @@ require_relative 'fixtures/classes'
describe "IO#close_write" do
before :each do
- @io = IO.popen 'cat', 'r+'
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ @io = IO.popen cmd, 'r+'
@path = tmp('io.close.txt')
end
@@ -48,12 +49,15 @@ describe "IO#close_write" do
io.should.closed?
end
- it "flushes and closes the write stream" do
- @io.puts '12345'
+ # Windows didn't have command like cat
+ platform_is_not :windows do
+ it "flushes and closes the write stream" do
+ @io.puts '12345'
- @io.close_write
+ @io.close_write
- @io.read.should == "12345\n"
+ @io.read.should == "12345\n"
+ end
end
it "does nothing on closed stream" do
diff --git a/spec/ruby/core/io/codepoints_spec.rb b/spec/ruby/core/io/codepoints_spec.rb
deleted file mode 100644
index 04c115dd3f..0000000000
--- a/spec/ruby/core/io/codepoints_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require_relative '../../spec_helper'
-require_relative 'fixtures/classes'
-require_relative 'shared/codepoints'
-
-ruby_version_is ''...'3.0' do
-
- # See redmine #1667
- describe "IO#codepoints" do
- before :each do
- @verbose, $VERBOSE = $VERBOSE, nil
- end
-
- after :each do
- $VERBOSE = @verbose
- end
-
- it_behaves_like :io_codepoints, :codepoints
- end
-
- describe "IO#codepoints" do
- before :each do
- @io = IOSpecs.io_fixture "lines.txt"
- @verbose, $VERBOSE = $VERBOSE, nil
- end
-
- after :each do
- $VERBOSE = @verbose
- @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
-end
diff --git a/spec/ruby/core/io/copy_stream_spec.rb b/spec/ruby/core/io/copy_stream_spec.rb
index df9c5c7390..ffa2ea992c 100644
--- a/spec/ruby/core/io/copy_stream_spec.rb
+++ b/spec/ruby/core/io/copy_stream_spec.rb
@@ -69,9 +69,12 @@ describe :io_copy_stream_to_io, shared: true do
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"
- -> { IO.copy_stream @object.from, @to_io }.should raise_error(IOError)
+ to_io = new_io __FILE__, "r"
+ begin
+ -> { IO.copy_stream @object.from, to_io }.should raise_error(IOError)
+ ensure
+ to_io.close
+ end
end
it "does not close the destination IO" do
@@ -109,7 +112,8 @@ describe "IO.copy_stream" do
end
after :each do
- rm_r @to_name, @from_bigfile
+ rm_r @to_name if @to_name
+ rm_r @from_bigfile
end
describe "from an IO" do
@@ -164,6 +168,25 @@ describe "IO.copy_stream" do
it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream
end
+
+ describe "to a Tempfile" do
+ before :all do
+ require 'tempfile'
+ end
+
+ before :each do
+ @to_io = Tempfile.new("rubyspec_copy_stream", encoding: Encoding::BINARY, mode: File::RDONLY)
+ @to_name = @to_io.path
+ end
+
+ after :each do
+ @to_io.close!
+ @to_name = nil # do not rm_r it, already done by Tempfile#close!
+ end
+
+ it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
+ it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream
+ end
end
describe "from a file name" do
@@ -277,10 +300,8 @@ describe "IO.copy_stream" do
@io.should_not_receive(:pos)
IO.copy_stream(@io, @to_name)
end
-
end
-
describe "with a destination that does partial reads" do
before do
@from_out, @from_in = IO.pipe
diff --git a/spec/ruby/core/io/eof_spec.rb b/spec/ruby/core/io/eof_spec.rb
index 315345d942..b4850df437 100644
--- a/spec/ruby/core/io/eof_spec.rb
+++ b/spec/ruby/core/io/eof_spec.rb
@@ -76,7 +76,7 @@ describe "IO#eof?" do
end
it "returns true on one-byte stream after single-byte read" do
- File.open(File.dirname(__FILE__) + '/fixtures/one_byte.txt') { |one_byte|
+ File.open(__dir__ + '/fixtures/one_byte.txt') { |one_byte|
one_byte.read(1)
one_byte.should.eof?
}
diff --git a/spec/ruby/core/io/fixtures/classes.rb b/spec/ruby/core/io/fixtures/classes.rb
index 5d81d5fcd9..204a2a101b 100644
--- a/spec/ruby/core/io/fixtures/classes.rb
+++ b/spec/ruby/core/io/fixtures/classes.rb
@@ -7,6 +7,18 @@ module IOSpecs
class SubIO < IO
end
+ class SubIOWithRedefinedNew < IO
+ def self.new(...)
+ ScratchPad << :redefined_new_called
+ super
+ end
+
+ def initialize(...)
+ ScratchPad << :call_original_initialize
+ super
+ end
+ end
+
def self.collector
Proc.new { |x| ScratchPad << x }
end
@@ -108,6 +120,14 @@ module IOSpecs
"linha ", "cinco.\nHere ", "is ", "line ", "six.\n" ]
end
+ def self.lines_space_separator_without_trailing_spaces
+ [ "Voici", "la", "ligne", "une.\nQui",
+ "\303\250", "la", "linea", "due.\n\n\nAqu\303\255",
+ "est\303\241", "la", "l\303\255nea", "tres.\nHier",
+ "ist", "Zeile", "vier.\n\nEst\303\241", "aqui", "a",
+ "linha", "cinco.\nHere", "is", "line", "six.\n" ]
+ end
+
def self.lines_arbitrary_separator
[ "Voici la ligne une.\nQui \303\250",
" la linea due.\n\n\nAqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\nEst\303\241 aqui a linha cinco.\nHere is line six.\n" ]
@@ -119,6 +139,12 @@ module IOSpecs
"Est\303\241 aqui a linha cinco.\nHere is line six.\n" ]
end
+ def self.paragraphs_without_trailing_new_line_characters
+ [ "Voici la ligne une.\nQui \303\250 la linea due.",
+ "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.",
+ "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ]
+ end
+
# Creates an IO instance for an existing fixture file. The
# file should obviously not be deleted.
def self.io_fixture(name, mode = "r:utf-8")
diff --git a/spec/ruby/core/io/flush_spec.rb b/spec/ruby/core/io/flush_spec.rb
index 34cf42c425..f7d5ba77fc 100644
--- a/spec/ruby/core/io/flush_spec.rb
+++ b/spec/ruby/core/io/flush_spec.rb
@@ -19,11 +19,11 @@ describe "IO#flush" do
end
end
- # [ruby-core:90895] MJIT worker may leave fd open in a forked child.
- # For instance, MJIT creates a worker before @r.close with fork(), @r.close happens,
- # and the MJIT worker keeps the pipe open until the worker execve().
- # TODO: consider acquiring GVL from MJIT worker.
- guard_not -> { defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? } do
+ # [ruby-core:90895] RJIT worker may leave fd open in a forked child.
+ # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens,
+ # and the RJIT worker keeps the pipe open until the worker execve().
+ # TODO: consider acquiring GVL from RJIT worker.
+ guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do
it "raises Errno::EPIPE if sync=false and the read end is closed" do
@w.sync = false
@w.write "foo"
diff --git a/spec/ruby/core/io/foreach_spec.rb b/spec/ruby/core/io/foreach_spec.rb
index c2276cf544..c361d27879 100644
--- a/spec/ruby/core/io/foreach_spec.rb
+++ b/spec/ruby/core/io/foreach_spec.rb
@@ -20,7 +20,10 @@ describe "IO.foreach" do
platform_is :windows do
cmd = "|cmd.exe /C echo hello&echo line2"
end
- IO.foreach(cmd) { |l| ScratchPad << l }
+
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.foreach(cmd) { |l| ScratchPad << l }
+ end
ScratchPad.recorded.should == ["hello\n", "line2\n"]
end
@@ -28,7 +31,9 @@ describe "IO.foreach" do
it "gets data from a fork when passed -" do
parent_pid = $$
- IO.foreach("|-") { |l| ScratchPad << l }
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.foreach("|-") { |l| ScratchPad << l }
+ end
if $$ == parent_pid
ScratchPad.recorded.should == ["hello\n", "from a fork\n"]
@@ -39,6 +44,16 @@ describe "IO.foreach" do
end
end
end
+
+ ruby_version_is "3.3" do
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ IO.foreach(cmd).to_a
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
end
end
diff --git a/spec/ruby/core/io/getbyte_spec.rb b/spec/ruby/core/io/getbyte_spec.rb
index 6ba8f0a3e0..b4351160e6 100644
--- a/spec/ruby/core/io/getbyte_spec.rb
+++ b/spec/ruby/core/io/getbyte_spec.rb
@@ -40,3 +40,19 @@ describe "IO#getbyte" do
@io.getbyte.should == nil
end
end
+
+describe "IO#getbyte" do
+ before :each do
+ @name = tmp("io_getbyte.txt")
+ @io = new_io(@name, 'w')
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name if @name
+ end
+
+ it "raises an IOError if the stream is not readable" do
+ -> { @io.getbyte }.should raise_error(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/gets_spec.rb b/spec/ruby/core/io/gets_spec.rb
index a3cd180b66..ca64bf860e 100644
--- a/spec/ruby/core/io/gets_spec.rb
+++ b/spec/ruby/core/io/gets_spec.rb
@@ -24,6 +24,12 @@ describe "IO#gets" do
end
end
+ it "sets $_ to nil after the last line has been read" do
+ while @io.gets
+ end
+ $_.should be_nil
+ end
+
it "returns nil if called at the end of the stream" do
IOSpecs.lines.length.times { @io.gets }
@io.gets.should == nil
@@ -38,14 +44,6 @@ describe "IO#gets" do
IOSpecs.lines.each { |line| line.should == @io.gets }
end
- ruby_version_is ''...'2.7' do
- it "returns tainted strings" do
- while line = @io.gets
- line.should.tainted?
- end
- end
- end
-
it "updates lineno with each invocation" do
while @io.gets
@io.lineno.should == @count += 1
@@ -64,14 +62,6 @@ describe "IO#gets" do
@io.gets(nil).should == IOSpecs.lines.join("")
end
- ruby_version_is ''...'2.7' do
- it "returns tainted strings" do
- while line = @io.gets(nil)
- line.should.tainted?
- end
- end
- end
-
it "updates lineno with each invocation" do
while @io.gets(nil)
@io.lineno.should == @count += 1
@@ -100,14 +90,6 @@ describe "IO#gets" do
@io.gets.should == IOSpecs.lines[4]
end
- ruby_version_is ''...'2.7' do
- it "returns tainted strings" do
- while line = @io.gets("")
- line.should.tainted?
- end
- end
- end
-
it "updates lineno with each invocation" do
while @io.gets("")
@io.lineno.should == @count += 1
@@ -126,14 +108,6 @@ describe "IO#gets" do
@io.gets("la linea").should == "Voici la ligne une.\nQui \303\250 la linea"
end
- ruby_version_is ''...'2.7' do
- it "returns tainted strings" do
- while line = @io.gets("la")
- line.should.tainted?
- end
- end
- end
-
it "updates lineno with each invocation" do
while (@io.gets("la"))
@io.lineno.should == @count += 1
@@ -145,12 +119,49 @@ describe "IO#gets" do
$..should == @count += 1
end
end
+
+ describe "that consists of multiple bytes" do
+ platform_is_not :windows do
+ it "should match the separator even if the buffer is filled over successive reads" do
+ IO.pipe do |read, write|
+
+ # Write part of the string with the separator split between two write calls. We want
+ # the read to intertwine such that when the read starts the full data isn't yet
+ # available in the buffer.
+ write.write("Aquí está la línea tres\r\n")
+
+ t = Thread.new do
+ # Continue reading until the separator is encountered or the pipe is closed.
+ read.gets("\r\n\r\n")
+ end
+
+ # Write the other half of the separator, which should cause the `gets` call to now
+ # match. Explicitly close the pipe for good measure so a bug in `gets` doesn't block forever.
+ Thread.pass until t.stop?
+
+ write.write("\r\nelse\r\n\r\n")
+ write.close
+
+ t.value.bytes.should == "Aquí está la línea tres\r\n\r\n".bytes
+ read.read(8).bytes.should == "else\r\n\r\n".bytes
+ end
+ end
+ end
+ end
end
describe "when passed chomp" do
it "returns the first line without a trailing newline character" do
@io.gets(chomp: true).should == IOSpecs.lines_without_newline_characters[0]
end
+
+ it "raises exception when options passed as Hash" do
+ -> { @io.gets({ chomp: true }) }.should raise_error(TypeError)
+
+ -> {
+ @io.gets("\n", 1, { chomp: true })
+ }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
end
end
@@ -232,6 +243,16 @@ describe "IO#gets" do
it "reads all bytes when pass a separator and reading more than all bytes" do
@io.gets("\t", 100).should == "one\n\ntwo\n\nthree\nfour\n"
end
+
+ it "returns empty string when 0 passed as a limit" do
+ @io.gets(0).should == ""
+ @io.gets(nil, 0).should == ""
+ @io.gets("", 0).should == ""
+ end
+
+ it "does not accept limit that doesn't fit in a C off_t" do
+ -> { @io.gets(2**128) }.should raise_error(RangeError)
+ end
end
describe "IO#gets" do
@@ -317,11 +338,23 @@ describe "IO#gets" do
@io.gets.encoding.should == Encoding::BINARY
end
- it "transcodes to internal encoding if the IO object's external encoding is BINARY" do
- Encoding.default_external = Encoding::BINARY
- Encoding.default_internal = Encoding::UTF_8
- @io = new_io @name, 'r'
- @io.set_encoding Encoding::BINARY, Encoding::UTF_8
- @io.gets.encoding.should == Encoding::UTF_8
+ ruby_version_is ''...'3.3' do
+ it "transcodes to internal encoding if the IO object's external encoding is BINARY" do
+ Encoding.default_external = Encoding::BINARY
+ Encoding.default_internal = Encoding::UTF_8
+ @io = new_io @name, 'r'
+ @io.set_encoding Encoding::BINARY, Encoding::UTF_8
+ @io.gets.encoding.should == Encoding::UTF_8
+ end
+ end
+
+ ruby_version_is '3.3' do
+ it "ignores the internal encoding if the IO object's external encoding is BINARY" do
+ Encoding.default_external = Encoding::BINARY
+ Encoding.default_internal = Encoding::UTF_8
+ @io = new_io @name, 'r'
+ @io.set_encoding Encoding::BINARY, Encoding::UTF_8
+ @io.gets.encoding.should == Encoding::BINARY
+ end
end
end
diff --git a/spec/ruby/core/io/initialize_spec.rb b/spec/ruby/core/io/initialize_spec.rb
index ba5bc117b7..026252a13d 100644
--- a/spec/ruby/core/io/initialize_spec.rb
+++ b/spec/ruby/core/io/initialize_spec.rb
@@ -27,6 +27,17 @@ describe "IO#initialize" do
@io.fileno.should == fd
end
+ it "accepts options as keyword arguments" do
+ fd = new_fd @name, "w:utf-8"
+
+ @io.send(:initialize, fd, "w", flags: File::CREAT)
+ @io.fileno.should == fd
+
+ -> {
+ @io.send(:initialize, fd, "w", {flags: File::CREAT})
+ }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 1..2)")
+ end
+
it "raises a TypeError when passed an IO" do
-> { @io.send :initialize, STDOUT, 'w' }.should raise_error(TypeError)
end
diff --git a/spec/ruby/core/io/ioctl_spec.rb b/spec/ruby/core/io/ioctl_spec.rb
index 8dcd9eb2c6..3f7b5ad5d7 100644
--- a/spec/ruby/core/io/ioctl_spec.rb
+++ b/spec/ruby/core/io/ioctl_spec.rb
@@ -12,7 +12,7 @@ describe "IO#ioctl" do
guard -> { RUBY_PLATFORM.include?("86") } do # x86 / x86_64
it "resizes an empty String to match the output size" do
File.open(__FILE__, 'r') do |f|
- buffer = ''
+ buffer = +''
# FIONREAD in /usr/include/asm-generic/ioctls.h
f.ioctl 0x541B, buffer
buffer.unpack('I').first.should be_kind_of(Integer)
diff --git a/spec/ruby/core/io/lineno_spec.rb b/spec/ruby/core/io/lineno_spec.rb
index 99266ecca1..e82cdd9f17 100644
--- a/spec/ruby/core/io/lineno_spec.rb
+++ b/spec/ruby/core/io/lineno_spec.rb
@@ -26,7 +26,8 @@ describe "IO#lineno" do
end
it "raises an IOError on a duplexed stream with the read side closed" do
- IO.popen('cat', 'r+') do |p|
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ IO.popen(cmd, 'r+') do |p|
p.close_read
-> { p.lineno }.should raise_error(IOError)
end
@@ -70,7 +71,8 @@ describe "IO#lineno=" do
end
it "raises an IOError on a duplexed stream with the read side closed" do
- IO.popen('cat', 'r+') do |p|
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ IO.popen(cmd, 'r+') do |p|
p.close_read
-> { p.lineno = 0 }.should raise_error(IOError)
end
@@ -92,8 +94,13 @@ describe "IO#lineno=" do
@io.lineno.should == 92233
end
- it "raises TypeError on nil argument" do
- -> { @io.lineno = nil }.should raise_error(TypeError)
+ it "raises TypeError if cannot convert argument to Integer implicitly" do
+ -> { @io.lineno = "1" }.should raise_error(TypeError, 'no implicit conversion of String into Integer')
+ -> { @io.lineno = nil }.should raise_error(TypeError, 'no implicit conversion from nil to integer')
+ end
+
+ it "does not accept Integers that don't fit in a C int" do
+ -> { @io.lineno = 2**32 }.should raise_error(RangeError)
end
it "sets the current line number to the given value" do
diff --git a/spec/ruby/core/io/lines_spec.rb b/spec/ruby/core/io/lines_spec.rb
deleted file mode 100644
index 5b29a1d07e..0000000000
--- a/spec/ruby/core/io/lines_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- encoding: utf-8 -*-
-require_relative '../../spec_helper'
-require_relative 'fixtures/classes'
-
-ruby_version_is ''...'3.0' do
- describe "IO#lines" do
- before :each do
- @io = IOSpecs.io_fixture "lines.txt"
- @verbose, $VERBOSE = $VERBOSE, nil
- end
-
- after :each do
- $VERBOSE = @verbose
- @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
-end
diff --git a/spec/ruby/core/io/new_spec.rb b/spec/ruby/core/io/new_spec.rb
index 3597098caf..979ac0efcb 100644
--- a/spec/ruby/core/io/new_spec.rb
+++ b/spec/ruby/core/io/new_spec.rb
@@ -1,8 +1,16 @@
require_relative '../../spec_helper'
require_relative 'shared/new'
+# NOTE: should be synchronized with library/stringio/initialize_spec.rb
+
describe "IO.new" do
it_behaves_like :io_new, :new
+
+ it "does not use the given block and warns to use IO::open" do
+ -> {
+ @io = IO.send(@method, @fd) { raise }
+ }.should complain(/warning: IO::new\(\) does not take block; use IO::open\(\) instead/)
+ end
end
describe "IO.new" do
diff --git a/spec/ruby/core/io/nonblock_spec.rb b/spec/ruby/core/io/nonblock_spec.rb
index e81ac10c58..99dc0cafd0 100644
--- a/spec/ruby/core/io/nonblock_spec.rb
+++ b/spec/ruby/core/io/nonblock_spec.rb
@@ -12,43 +12,21 @@ platform_is_not :windows do
end
end
- ruby_version_is ""..."3.0" do
- it "returns false for pipe by default" do
- r, w = IO.pipe
- begin
- r.nonblock?.should == false
- w.nonblock?.should == false
- ensure
- r.close
- w.close
- end
- end
-
- it "returns false for socket by default" do
- require 'socket'
- TCPServer.open(0) do |socket|
- socket.nonblock?.should == false
- end
+ it "returns true for pipe by default" do
+ r, w = IO.pipe
+ begin
+ r.nonblock?.should == true
+ w.nonblock?.should == true
+ ensure
+ r.close
+ w.close
end
end
- ruby_version_is "3.0" do
- it "returns true for pipe by default" do
- r, w = IO.pipe
- begin
- r.nonblock?.should == true
- w.nonblock?.should == true
- ensure
- r.close
- w.close
- end
- end
-
- it "returns true for socket by default" do
- require 'socket'
- TCPServer.open(0) do |socket|
- socket.nonblock?.should == true
- end
+ it "returns true for socket by default" do
+ require 'socket'
+ TCPServer.open(0) do |socket|
+ socket.nonblock?.should == true
end
end
end
diff --git a/spec/ruby/core/io/open_spec.rb b/spec/ruby/core/io/open_spec.rb
index d3a3961df7..d151da9ce5 100644
--- a/spec/ruby/core/io/open_spec.rb
+++ b/spec/ruby/core/io/open_spec.rb
@@ -37,6 +37,19 @@ describe "IO.open" do
ScratchPad.recorded.should == :called
end
+ it "propagate an exception in the block after calling #close" do
+ -> do
+ IO.open(@fd, "w") do |io|
+ IOSpecs.io_mock(io, :close) do
+ super()
+ ScratchPad.record :called
+ end
+ raise Exception
+ end
+ end.should raise_error(Exception)
+ ScratchPad.recorded.should == :called
+ end
+
it "propagates an exception raised by #close that is not a StandardError" do
-> do
IO.open(@fd, "w") do |io|
diff --git a/spec/ruby/core/io/path_spec.rb b/spec/ruby/core/io/path_spec.rb
new file mode 100644
index 0000000000..8145c32f39
--- /dev/null
+++ b/spec/ruby/core/io/path_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "IO#path" do
+ ruby_version_is "3.2" do
+ it "returns the path of the file associated with the IO object" do
+ path = tmp("io_path.txt")
+ File.open(path, "w") do |file|
+ IO.new(file.fileno, path: file.path, autoclose: false).path.should == file.path
+ end
+ ensure
+ File.unlink(path)
+ end
+ end
+end
diff --git a/spec/ruby/core/io/pipe_spec.rb b/spec/ruby/core/io/pipe_spec.rb
index 2f2cf06f4d..aee0d9003f 100644
--- a/spec/ruby/core/io/pipe_spec.rb
+++ b/spec/ruby/core/io/pipe_spec.rb
@@ -25,6 +25,17 @@ describe "IO.pipe" do
@r.should be_an_instance_of(IOSpecs::SubIO)
@w.should be_an_instance_of(IOSpecs::SubIO)
end
+
+ it "does not use IO.new method to create pipes and allows its overriding" do
+ ScratchPad.record []
+
+ # so redefined .new is not called, but original #initialize is
+ @r, @w = IOSpecs::SubIOWithRedefinedNew.pipe
+ ScratchPad.recorded.should == [:call_original_initialize, :call_original_initialize] # called 2 times - for each pipe (r and w)
+
+ @r.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew)
+ @w.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew)
+ end
end
describe "IO.pipe" do
diff --git a/spec/ruby/core/io/pread_spec.rb b/spec/ruby/core/io/pread_spec.rb
index 43071d6a31..28afc80e5c 100644
--- a/spec/ruby/core/io/pread_spec.rb
+++ b/spec/ruby/core/io/pread_spec.rb
@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
require_relative '../../spec_helper'
-platform_is_not :windows do
+guard -> { platform_is_not :windows or ruby_version_is "3.3" } do
describe "IO#pread" do
before :each do
@fname = tmp("io_pread.txt")
@@ -21,16 +21,93 @@ platform_is_not :windows do
end
it "accepts a length, an offset, and an output buffer" do
- buffer = "foo"
+ buffer = +"foo"
@file.pread(3, 4, buffer)
buffer.should == "567"
end
+ it "shrinks the buffer in case of less bytes read" do
+ buffer = +"foo"
+ @file.pread(1, 0, buffer)
+ buffer.should == "1"
+ end
+
+ it "grows the buffer in case of more bytes read" do
+ buffer = +"foo"
+ @file.pread(5, 0, buffer)
+ buffer.should == "12345"
+ end
+
it "does not advance the file pointer" do
@file.pread(4, 0).should == "1234"
@file.read.should == "1234567890"
end
+ it "ignores the current offset" do
+ @file.pos = 3
+ @file.pread(4, 0).should == "1234"
+ end
+
+ it "returns an empty string for maxlen = 0" do
+ @file.pread(0, 4).should == ""
+ end
+
+ it "ignores the offset for maxlen = 0, even if it is out of file bounds" do
+ @file.pread(0, 400).should == ""
+ end
+
+ it "does not reset the buffer when reading with maxlen = 0" do
+ buffer = +"foo"
+ @file.pread(0, 4, buffer)
+ buffer.should == "foo"
+
+ @file.pread(0, 400, buffer)
+ buffer.should == "foo"
+ end
+
+ it "converts maxlen to Integer using #to_int" do
+ maxlen = mock('maxlen')
+ maxlen.should_receive(:to_int).and_return(4)
+ @file.pread(maxlen, 0).should == "1234"
+ end
+
+ it "converts offset to Integer using #to_int" do
+ offset = mock('offset')
+ offset.should_receive(:to_int).and_return(0)
+ @file.pread(4, offset).should == "1234"
+ end
+
+ it "converts a buffer to String using to_str" do
+ buffer = mock('buffer')
+ buffer.should_receive(:to_str).at_least(1).and_return(+"foo")
+ @file.pread(4, 0, buffer)
+ buffer.should_not.is_a?(String)
+ buffer.to_str.should == "1234"
+ end
+
+ it "raises TypeError if maxlen is not an Integer and cannot be coerced into Integer" do
+ maxlen = Object.new
+ -> { @file.pread(maxlen, 0) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer')
+ end
+
+ it "raises TypeError if offset is not an Integer and cannot be coerced into Integer" do
+ offset = Object.new
+ -> { @file.pread(4, offset) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer')
+ end
+
+ it "raises ArgumentError for negative values of maxlen" do
+ -> { @file.pread(-4, 0) }.should raise_error(ArgumentError, 'negative string size (or size too big)')
+ end
+
+ it "raised Errno::EINVAL for negative values of offset" do
+ -> { @file.pread(4, -1) }.should raise_error(Errno::EINVAL, /Invalid argument/)
+ end
+
+ it "raises TypeError if the buffer is not a String and cannot be coerced into String" do
+ buffer = Object.new
+ -> { @file.pread(4, 0, buffer) }.should raise_error(TypeError, 'no implicit conversion of Object into String')
+ end
+
it "raises EOFError if end-of-file is reached" do
-> { @file.pread(1, 10) }.should raise_error(EOFError)
end
diff --git a/spec/ruby/core/io/print_spec.rb b/spec/ruby/core/io/print_spec.rb
index 04e971ef6d..085852024c 100644
--- a/spec/ruby/core/io/print_spec.rb
+++ b/spec/ruby/core/io/print_spec.rb
@@ -3,16 +3,27 @@ require_relative 'fixtures/classes'
describe "IO#print" do
before :each do
- @old_separator = $\
- suppress_warning {$\ = '->'}
+ @old_record_separator = $\
+ @old_field_separator = $,
+ suppress_warning {
+ $\ = '->'
+ $, = '^^'
+ }
@name = tmp("io_print")
end
after :each do
- suppress_warning {$\ = @old_separator}
+ suppress_warning {
+ $\ = @old_record_separator
+ $, = @old_field_separator
+ }
rm_r @name
end
+ it "returns nil" do
+ touch(@name) { |f| f.print.should be_nil }
+ end
+
it "writes $_.to_s followed by $\\ (if any) to the stream if no arguments given" do
o = mock('o')
o.should_receive(:to_s).and_return("mockmockmock")
@@ -38,13 +49,15 @@ describe "IO#print" do
IO.read(@name).should == "hello#{$\}"
end
- it "writes each obj.to_s to the stream and appends $\\ (if any) given multiple objects" do
+ it "writes each obj.to_s to the stream separated by $, (if any) and appends $\\ (if any) given multiple objects" do
o, o2 = Object.new, Object.new
def o.to_s(); 'o'; end
def o2.to_s(); 'o2'; end
- touch(@name) { |f| f.print(o, o2) }
- IO.read(@name).should == "#{o.to_s}#{o2.to_s}#{$\}"
+ suppress_warning {
+ touch(@name) { |f| f.print(o, o2) }
+ }
+ IO.read(@name).should == "#{o.to_s}#{$,}#{o2.to_s}#{$\}"
end
it "raises IOError on closed stream" do
diff --git a/spec/ruby/core/io/puts_spec.rb b/spec/ruby/core/io/puts_spec.rb
index 9a708fffef..9ed343c94c 100644
--- a/spec/ruby/core/io/puts_spec.rb
+++ b/spec/ruby/core/io/puts_spec.rb
@@ -6,7 +6,7 @@ describe "IO#puts" do
@before_separator = $/
@name = tmp("io_puts.txt")
@io = new_io @name
- ScratchPad.record ""
+ ScratchPad.record(+"")
def @io.write(str)
ScratchPad << str
end
diff --git a/spec/ruby/core/io/pwrite_spec.rb b/spec/ruby/core/io/pwrite_spec.rb
index fe29d1e1f6..2bc508b37d 100644
--- a/spec/ruby/core/io/pwrite_spec.rb
+++ b/spec/ruby/core/io/pwrite_spec.rb
@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
require_relative '../../spec_helper'
-platform_is_not :windows do
+guard -> { platform_is_not :windows or ruby_version_is "3.3" } do
describe "IO#pwrite" do
before :each do
@fname = tmp("io_pwrite.txt")
@@ -28,16 +28,42 @@ platform_is_not :windows do
@file.pread(6, 0).should == "foobar"
end
+ it "calls #to_s on the object to be written" do
+ object = mock("to_s")
+ object.should_receive(:to_s).and_return("foo")
+ @file.pwrite(object, 0)
+ @file.pread(3, 0).should == "foo"
+ end
+
+ it "calls #to_int on the offset" do
+ offset = mock("to_int")
+ offset.should_receive(:to_int).and_return(2)
+ @file.pwrite("foo", offset)
+ @file.pread(3, 2).should == "foo"
+ end
+
it "raises IOError when file is not open in write mode" do
File.open(@fname, "r") do |file|
- -> { file.pwrite("foo", 1) }.should raise_error(IOError)
+ -> { file.pwrite("foo", 1) }.should raise_error(IOError, "not opened for writing")
end
end
it "raises IOError when file is closed" do
file = File.open(@fname, "w+")
file.close
- -> { file.pwrite("foo", 1) }.should raise_error(IOError)
+ -> { file.pwrite("foo", 1) }.should raise_error(IOError, "closed stream")
+ end
+
+ it "raises a NoMethodError if object does not respond to #to_s" do
+ -> {
+ @file.pwrite(BasicObject.new, 0)
+ }.should raise_error(NoMethodError, /undefined method [`']to_s'/)
+ end
+
+ it "raises a TypeError if the offset cannot be converted to an Integer" do
+ -> {
+ @file.pwrite("foo", Object.new)
+ }.should raise_error(TypeError, "no implicit conversion of Object into Integer")
end
end
end
diff --git a/spec/ruby/core/io/read_nonblock_spec.rb b/spec/ruby/core/io/read_nonblock_spec.rb
index e50531d336..51e7cd6bd2 100644
--- a/spec/ruby/core/io/read_nonblock_spec.rb
+++ b/spec/ruby/core/io/read_nonblock_spec.rb
@@ -55,6 +55,27 @@ describe "IO#read_nonblock" do
@read.read_nonblock(4).should == "hell"
end
+ it "reads after ungetc with data in the buffer" do
+ @write.write("foobar")
+ @read.set_encoding(
+ 'utf-8', universal_newline: false
+ )
+ c = @read.getc
+ @read.ungetc(c)
+ @read.read_nonblock(3).should == "foo"
+ @read.read_nonblock(3).should == "bar"
+ end
+
+ it "raises an exception after ungetc with data in the buffer and character conversion enabled" do
+ @write.write("foobar")
+ @read.set_encoding(
+ 'utf-8', universal_newline: true
+ )
+ c = @read.getc
+ @read.ungetc(c)
+ -> { @read.read_nonblock(3).should == "foo" }.should raise_error(IOError)
+ end
+
it "returns less data if that is all that is available" do
@write << "hello"
@read.read_nonblock(10).should == "hello"
@@ -70,20 +91,39 @@ describe "IO#read_nonblock" do
@read.read_nonblock(1).should == "1"
end
+ it "raises ArgumentError when length is less than 0" do
+ -> { @read.read_nonblock(-1) }.should raise_error(ArgumentError)
+ end
+
it "reads into the passed buffer" do
- buffer = ""
+ buffer = +""
@write.write("1")
@read.read_nonblock(1, buffer)
buffer.should == "1"
end
it "returns the passed buffer" do
- buffer = ""
+ buffer = +""
@write.write("1")
output = @read.read_nonblock(1, buffer)
output.should equal(buffer)
end
+ it "discards the existing buffer content upon successful read" do
+ buffer = +"existing content"
+ @write.write("hello world")
+ @write.close
+ @read.read_nonblock(11, buffer)
+ buffer.should == "hello world"
+ end
+
+ it "discards the existing buffer content upon error" do
+ buffer = +"existing content"
+ @write.close
+ -> { @read.read_nonblock(1, buffer) }.should raise_error(EOFError)
+ buffer.should be_empty
+ end
+
it "raises IOError on closed stream" do
-> { IOSpecs.closed_io.read_nonblock(5) }.should raise_error(IOError)
end
@@ -96,4 +136,13 @@ describe "IO#read_nonblock" do
-> { @read.read_nonblock(5) }.should raise_error(EOFError)
end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ @write.write("abc")
+ @write.close
+ @read.read_nonblock(10, buffer)
+
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
end
diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb
index 841e693f37..eb3652e692 100644
--- a/spec/ruby/core/io/read_spec.rb
+++ b/spec/ruby/core/io/read_spec.rb
@@ -23,6 +23,15 @@ describe "IO.read" do
IO.read(p)
end
+ # https://bugs.ruby-lang.org/issues/19354
+ it "accepts options as keyword arguments" do
+ IO.read(@fname, 3, 0, mode: "r+").should == @contents[0, 3]
+
+ -> {
+ IO.read(@fname, 3, 0, {mode: "r+"})
+ }.should raise_error(ArgumentError, /wrong number of arguments/)
+ end
+
it "accepts an empty options Hash" do
IO.read(@fname, **{}).should == @contents
end
@@ -55,6 +64,33 @@ describe "IO.read" do
IO.read(@fname, mode: "a+").should == @contents
end
+ platform_is_not :windows do
+ ruby_version_is ""..."3.3" do
+ it "uses an :open_args option" do
+ string = IO.read(@fname, nil, 0, open_args: ["r", nil, {encoding: Encoding::US_ASCII}])
+ string.encoding.should == Encoding::US_ASCII
+
+ string = IO.read(@fname, nil, 0, open_args: ["r", nil, {}])
+ string.encoding.should == Encoding::UTF_8
+ end
+ end
+ end
+
+ it "disregards other options if :open_args is given" do
+ string = IO.read(@fname,mode: "w", encoding: Encoding::UTF_32LE, open_args: ["r", encoding: Encoding::UTF_8])
+ string.encoding.should == Encoding::UTF_8
+ end
+
+ it "doesn't require mode to be specified in :open_args" do
+ string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII}])
+ string.encoding.should == Encoding::US_ASCII
+ end
+
+ it "doesn't require mode to be specified in :open_args even if flags option passed" do
+ string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII, flags: File::CREAT}])
+ string.encoding.should == Encoding::US_ASCII
+ end
+
it "treats second nil argument as no length limit" do
IO.read(@fname, nil).should == @contents
IO.read(@fname, nil, 5).should == IO.read(@fname, @contents.length, 5)
@@ -77,6 +113,15 @@ describe "IO.read" do
IO.read(@fname, 1, 10).should == nil
end
+ it "returns an empty string when reading zero bytes" do
+ IO.read(@fname, 0).should == ''
+ end
+
+ it "returns a String in BINARY when passed a size" do
+ IO.read(@fname, 1).encoding.should == Encoding::BINARY
+ IO.read(@fname, 0).encoding.should == Encoding::BINARY
+ end
+
it "raises an Errno::ENOENT when the requested file does not exist" do
rm_r @fname
-> { IO.read @fname }.should raise_error(Errno::ENOENT)
@@ -90,9 +135,18 @@ describe "IO.read" do
-> { IO.read @fname, -1 }.should raise_error(ArgumentError)
end
- it "raises an Errno::EINVAL when not passed a valid offset" do
- -> { IO.read @fname, 0, -1 }.should raise_error(Errno::EINVAL)
- -> { IO.read @fname, -1, -1 }.should raise_error(Errno::EINVAL)
+ ruby_version_is ''...'3.3' do
+ it "raises an Errno::EINVAL when not passed a valid offset" do
+ -> { IO.read @fname, 0, -1 }.should raise_error(Errno::EINVAL)
+ -> { IO.read @fname, -1, -1 }.should raise_error(Errno::EINVAL)
+ end
+ end
+
+ ruby_version_is '3.3' do
+ it "raises an ArgumentError when not passed a valid offset" do
+ -> { IO.read @fname, 0, -1 }.should raise_error(ArgumentError)
+ -> { IO.read @fname, -1, -1 }.should raise_error(ArgumentError)
+ end
end
it "uses the external encoding specified via the :external_encoding option" do
@@ -104,6 +158,14 @@ describe "IO.read" do
str = IO.read(@fname, encoding: Encoding::ISO_8859_1)
str.encoding.should == Encoding::ISO_8859_1
end
+
+ platform_is :windows do
+ it "reads the file in text mode" do
+ # 0x1A is CTRL+Z and is EOF in Windows text mode.
+ File.binwrite(@fname, "\x1Abbb")
+ IO.read(@fname).should.empty?
+ end
+ end
end
describe "IO.read from a pipe" do
@@ -112,12 +174,19 @@ describe "IO.read from a pipe" do
platform_is :windows do
cmd = "|cmd.exe /C echo hello"
end
- IO.read(cmd).should == "hello\n"
+
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read(cmd).should == "hello\n"
+ end
end
platform_is_not :windows do
it "opens a pipe to a fork if the rest is -" do
- str = IO.read("|-")
+ str = nil
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ str = IO.read("|-")
+ end
+
if str # parent
str.should == "hello from child\n"
else #child
@@ -132,13 +201,18 @@ describe "IO.read from a pipe" do
platform_is :windows do
cmd = "|cmd.exe /C echo hello"
end
- IO.read(cmd, 1).should == "h"
+
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read(cmd, 1).should == "h"
+ end
end
platform_is_not :windows do
it "raises Errno::ESPIPE if passed an offset" do
-> {
- IO.read("|sh -c 'echo hello'", 1, 1)
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read("|sh -c 'echo hello'", 1, 1)
+ end
}.should raise_error(Errno::ESPIPE)
end
end
@@ -149,11 +223,23 @@ quarantine! do # The process tried to write to a nonexistent pipe.
# once https://bugs.ruby-lang.org/issues/12230 is fixed.
it "raises Errno::EINVAL if passed an offset" do
-> {
- IO.read("|cmd.exe /C echo hello", 1, 1)
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read("|cmd.exe /C echo hello", 1, 1)
+ end
}.should raise_error(Errno::EINVAL)
end
end
end
+
+ ruby_version_is "3.3" do
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ IO.read(cmd)
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
end
describe "IO.read on an empty file" do
@@ -197,21 +283,55 @@ describe "IO#read" do
@io.read(4).should == '7890'
end
+ it "treats first nil argument as no length limit" do
+ @io.read(nil).should == @contents
+ end
+
+ it "raises an ArgumentError when not passed a valid length" do
+ -> { @io.read(-1) }.should raise_error(ArgumentError)
+ end
+
it "clears the output buffer if there is nothing to read" do
@io.pos = 10
- buf = 'non-empty string'
+ buf = +'non-empty string'
@io.read(10, buf).should == nil
buf.should == ''
+
+ buf = +'non-empty string'
+
+ @io.read(nil, buf).should == ""
+
+ buf.should == ''
+
+ buf = +'non-empty string'
+
+ @io.read(0, buf).should == ""
+
+ buf.should == ''
+ end
+
+ it "raise FrozenError if the output buffer is frozen" do
+ @io.read
+ -> { @io.read(0, 'frozen-string'.freeze) }.should raise_error(FrozenError)
+ -> { @io.read(1, 'frozen-string'.freeze) }.should raise_error(FrozenError)
+ -> { @io.read(nil, 'frozen-string'.freeze) }.should raise_error(FrozenError)
+ end
+
+ ruby_bug "", ""..."3.3" do
+ it "raise FrozenError if the output buffer is frozen (2)" do
+ @io.read
+ -> { @io.read(1, ''.freeze) }.should raise_error(FrozenError)
+ end
end
it "consumes zero bytes when reading zero bytes" do
@io.read(0).should == ''
@io.pos.should == 0
- @io.getc.chr.should == '1'
+ @io.getc.should == '1'
end
it "is at end-of-file when everything has been read" do
@@ -224,46 +344,53 @@ describe "IO#read" do
end
it "places the specified number of bytes in the buffer" do
- buf = ""
+ buf = +""
@io.read 5, buf
buf.should == "12345"
end
it "expands the buffer when too small" do
- buf = "ABCDE"
+ buf = +"ABCDE"
@io.read nil, buf
buf.should == @contents
end
it "overwrites the buffer" do
- buf = "ABCDEFGHIJ"
+ buf = +"ABCDEFGHIJ"
@io.read nil, buf
buf.should == @contents
end
it "truncates the buffer when too big" do
- buf = "ABCDEFGHIJKLMNO"
+ buf = +"ABCDEFGHIJKLMNO"
@io.read nil, buf
buf.should == @contents
@io.rewind
- buf = "ABCDEFGHIJKLMNO"
+ buf = +"ABCDEFGHIJKLMNO"
@io.read 5, buf
buf.should == @contents[0..4]
end
it "returns the given buffer" do
- buf = ""
+ buf = +""
@io.read(nil, buf).should equal buf
end
+ it "returns the given buffer when there is nothing to read" do
+ buf = +""
+
+ @io.read
+ @io.read(nil, buf).should equal buf
+ end
+
it "coerces the second argument to string and uses it as a buffer" do
- buf = "ABCDE"
+ buf = +"ABCDE"
obj = mock("buff")
obj.should_receive(:to_str).any_number_of_times.and_return(buf)
@@ -304,6 +431,9 @@ describe "IO#read" do
-> { IOSpecs.closed_io.read }.should raise_error(IOError)
end
+ it "raises ArgumentError when length is less than 0" do
+ -> { @io.read(-1) }.should raise_error(ArgumentError)
+ end
platform_is_not :windows do
it "raises IOError when stream is closed by another thread" do
@@ -384,13 +514,6 @@ describe "IO#read in binary mode" do
xE2 = [226].pack('C*')
result.should == ("abc" + xE2 + "def").force_encoding(Encoding::BINARY)
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::BINARY
- xE2 = [226].pack('C*')
- result.should == ("abc" + xE2 + "def").force_encoding(Encoding::BINARY)
- end
end
describe "IO#read in text mode" do
@@ -465,13 +588,13 @@ describe :io_read_internal_encoding, shared: true do
describe "when passed nil for limit" do
it "sets the buffer to a transcoded String" do
- result = @io.read(nil, buf = "")
+ 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
+ buf = "".dup.force_encoding Encoding::ISO_8859_1
@io.read(nil, buf)
buf.encoding.should equal(Encoding::UTF_8)
end
@@ -485,17 +608,18 @@ describe :io_read_size_internal_encoding, shared: true do
it "returns a String in BINARY when passed a size" do
@io.read(4).encoding.should equal(Encoding::BINARY)
+ @io.read(0).encoding.should equal(Encoding::BINARY)
end
it "does not change the buffer's encoding when passed a limit" do
- buf = "".force_encoding Encoding::ISO_8859_1
+ buf = "".dup.force_encoding Encoding::ISO_8859_1
@io.read(4, buf)
buf.should == [164, 162, 164, 234].pack('C*').force_encoding(Encoding::ISO_8859_1)
buf.encoding.should equal(Encoding::ISO_8859_1)
end
it "truncates the buffer but does not change the buffer's encoding when no data remains" do
- buf = "abc".force_encoding Encoding::ISO_8859_1
+ buf = "abc".dup.force_encoding Encoding::ISO_8859_1
@io.read
@io.read(1, buf).should be_nil
diff --git a/spec/ruby/core/io/readchar_spec.rb b/spec/ruby/core/io/readchar_spec.rb
index b5f762a846..a66773851a 100644
--- a/spec/ruby/core/io/readchar_spec.rb
+++ b/spec/ruby/core/io/readchar_spec.rb
@@ -1,6 +1,16 @@
require_relative '../../spec_helper'
require_relative 'fixtures/classes'
+describe :io_readchar_internal_encoding, shared: true do
+ it "returns a transcoded String" do
+ @io.readchar.should == "あ"
+ end
+
+ it "sets the String encoding to the internal encoding" do
+ @io.readchar.encoding.should equal(Encoding::UTF_8)
+ end
+end
+
describe "IO#readchar" do
before :each do
@io = IOSpecs.io_fixture "lines.txt"
@@ -29,6 +39,62 @@ describe "IO#readchar" do
end
end
+describe "IO#readchar with internal encoding" do
+ after :each do
+ @io.close if @io
+ end
+
+ describe "not specified" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp"
+ end
+
+ it "does not transcode the String" do
+ @io.readchar.should == ("あ").encode(Encoding::EUC_JP)
+ end
+
+ it "sets the String encoding to the external encoding" do
+ @io.readchar.encoding.should equal(Encoding::EUC_JP)
+ end
+ end
+
+ describe "specified by open mode" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8"
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+
+ describe "specified by mode: option" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8"
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+
+ describe "specified by internal_encoding: option" do
+ before :each do
+ options = { mode: "r",
+ internal_encoding: "utf-8",
+ external_encoding: "euc-jp" }
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", options
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+
+ describe "specified by encoding: option" do
+ before :each do
+ options = { mode: "r", encoding: "euc-jp:utf-8" }
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", options
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+end
+
describe "IO#readchar" do
before :each do
@io = IOSpecs.io_fixture "empty.txt"
diff --git a/spec/ruby/core/io/readline_spec.rb b/spec/ruby/core/io/readline_spec.rb
index 7cb1601816..a814c1be90 100644
--- a/spec/ruby/core/io/readline_spec.rb
+++ b/spec/ruby/core/io/readline_spec.rb
@@ -43,9 +43,42 @@ describe "IO#readline" do
end
end
+ describe "when passed limit" do
+ it "reads limit bytes" do
+ @io.readline(3).should == "Voi"
+ end
+
+ it "returns an empty string when passed 0 as a limit" do
+ @io.readline(0).should == ""
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { @io.readline(2**128) }.should raise_error(RangeError)
+ end
+ end
+
+ describe "when passed separator and limit" do
+ it "reads limit bytes till the separator" do
+ # Voici la ligne une.\
+ @io.readline(" ", 4).should == "Voic"
+ @io.readline(" ", 4).should == "i "
+ @io.readline(" ", 4).should == "la "
+ @io.readline(" ", 4).should == "lign"
+ @io.readline(" ", 4).should == "e "
+ end
+ end
+
describe "when passed chomp" do
it "returns the first line without a trailing newline character" do
@io.readline(chomp: true).should == IOSpecs.lines_without_newline_characters[0]
end
+
+ it "raises exception when options passed as Hash" do
+ -> { @io.readline({ chomp: true }) }.should raise_error(TypeError)
+
+ -> {
+ @io.readline("\n", 1, { chomp: true })
+ }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
end
end
diff --git a/spec/ruby/core/io/readlines_spec.rb b/spec/ruby/core/io/readlines_spec.rb
index 254f2927b5..3a6ff3d0f3 100644
--- a/spec/ruby/core/io/readlines_spec.rb
+++ b/spec/ruby/core/io/readlines_spec.rb
@@ -101,6 +101,36 @@ describe "IO#readlines" do
@io.readlines(obj).should == IOSpecs.lines_r_separator
end
end
+
+ describe "when passed limit" do
+ it "raises ArgumentError when passed 0 as a limit" do
+ -> { @io.readlines(0) }.should raise_error(ArgumentError)
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { @io.readlines(2**128) }.should raise_error(RangeError)
+ end
+ end
+
+ describe "when passed chomp" do
+ it "returns the first line without a trailing newline character" do
+ @io.readlines(chomp: true).should == IOSpecs.lines_without_newline_characters
+ end
+
+ it "raises exception when options passed as Hash" do
+ -> { @io.readlines({ chomp: true }) }.should raise_error(TypeError)
+
+ -> {
+ @io.readlines("\n", 1, { chomp: true })
+ }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
+ end
+
+ describe "when passed arbitrary keyword argument" do
+ it "tolerates it" do
+ @io.readlines(chomp: true, foo: :bar).should == IOSpecs.lines_without_newline_characters
+ end
+ end
end
describe "IO#readlines" do
@@ -150,13 +180,20 @@ describe "IO.readlines" do
platform_is :windows do
cmd = "|cmd.exe /C echo hello&echo line2"
end
- lines = IO.readlines(cmd)
+
+ lines = nil
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ lines = IO.readlines(cmd)
+ end
lines.should == ["hello\n", "line2\n"]
end
platform_is_not :windows do
it "gets data from a fork when passed -" do
- lines = IO.readlines("|-")
+ lines = nil
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ lines = IO.readlines("|-")
+ end
if lines # parent
lines.should == ["hello\n", "from a fork\n"]
@@ -169,6 +206,16 @@ describe "IO.readlines" do
end
end
+ ruby_version_is "3.3" do
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ IO.readlines(cmd)
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
+
it_behaves_like :io_readlines, :readlines
it_behaves_like :io_readlines_options_19, :readlines
end
diff --git a/spec/ruby/core/io/readpartial_spec.rb b/spec/ruby/core/io/readpartial_spec.rb
index 2b33a0d5b1..0060beb545 100644
--- a/spec/ruby/core/io/readpartial_spec.rb
+++ b/spec/ruby/core/io/readpartial_spec.rb
@@ -59,7 +59,7 @@ describe "IO#readpartial" do
end
it "discards the existing buffer content upon successful read" do
- buffer = "existing"
+ buffer = +"existing content"
@wr.write("hello world")
@wr.close
@rd.readpartial(11, buffer)
@@ -74,7 +74,7 @@ describe "IO#readpartial" do
end
it "discards the existing buffer content upon error" do
- buffer = 'hello'
+ buffer = +'hello'
@wr.close
-> { @rd.readpartial(1, buffer) }.should raise_error(EOFError)
buffer.should be_empty
@@ -93,4 +93,19 @@ describe "IO#readpartial" do
@rd.readpartial(0).should == ""
end
+ ruby_bug "#18421", ""..."3.0.4" do
+ it "clears and returns the given buffer if the length argument is 0" do
+ buffer = +"existing content"
+ @rd.readpartial(0, buffer).should == buffer
+ buffer.should == ""
+ end
+ end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ @wr.write("abc")
+ @wr.close
+ @rd.readpartial(10, buffer)
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
end
diff --git a/spec/ruby/core/io/rewind_spec.rb b/spec/ruby/core/io/rewind_spec.rb
index 649041afaf..5579cbd988 100644
--- a/spec/ruby/core/io/rewind_spec.rb
+++ b/spec/ruby/core/io/rewind_spec.rb
@@ -18,6 +18,17 @@ describe "IO#rewind" do
@io.readline.should == "Voici la ligne une.\n"
end
+ it "positions the instance to the beginning of output for write-only IO" do
+ name = tmp("io_rewind_spec")
+ io = File.open(name, "w")
+ io.write("Voici la ligne une.\n")
+ io.rewind
+ io.pos.should == 0
+ ensure
+ io.close
+ rm_r name
+ end
+
it "positions the instance to the beginning of input and clears EOF" do
value = @io.read
@io.rewind
@@ -32,6 +43,10 @@ describe "IO#rewind" do
@io.lineno.should == 0
end
+ it "returns 0" do
+ @io.rewind.should == 0
+ end
+
it "raises IOError on closed stream" do
-> { IOSpecs.closed_io.rewind }.should raise_error(IOError)
end
diff --git a/spec/ruby/core/io/select_spec.rb b/spec/ruby/core/io/select_spec.rb
index 4603c1fbbc..3893e7620f 100644
--- a/spec/ruby/core/io/select_spec.rb
+++ b/spec/ruby/core/io/select_spec.rb
@@ -55,8 +55,8 @@ describe "IO.select" do
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
+ it "returns supplied objects correctly when monitoring the same object in different arrays" do
+ filename = tmp("IO_select_pipe_file")
io = File.open(filename, 'w+')
result = IO.select [io], [io], nil, 0
result.should == [[io], [io], []]
@@ -64,6 +64,17 @@ describe "IO.select" do
rm_r filename
end
+ it "returns the pipe read end in read set if the pipe write end is closed concurrently" do
+ main = Thread.current
+ t = Thread.new {
+ Thread.pass until main.stop?
+ @wr.close
+ }
+ IO.select([@rd]).should == [[@rd], [], []]
+ ensure
+ t.join
+ end
+
it "invokes to_io on supplied objects that are not IO and returns the supplied objects" do
# make some data available
@wr.write("foobar")
@@ -103,6 +114,39 @@ describe "IO.select" do
it "raises an ArgumentError when passed a negative timeout" do
-> { IO.select(nil, nil, nil, -5)}.should raise_error(ArgumentError)
end
+
+ describe "returns the available descriptors when the file descriptor" do
+ it "is in both read and error arrays" do
+ @wr.write("foobar")
+ result = IO.select([@rd], nil, [@rd])
+ result.should == [[@rd], [], []]
+ end
+
+ it "is in both write and error arrays" do
+ result = IO.select(nil, [@wr], [@wr])
+ result.should == [[], [@wr], []]
+ end
+
+ it "is in both read and write arrays" do
+ filename = tmp("IO_select_read_write_file")
+ w = File.open(filename, 'w+')
+ begin
+ IO.select([w], [w], []).should == [[w], [w], []]
+ ensure
+ w.close
+ rm_r filename
+ end
+
+ IO.select([@wr], [@wr], []).should == [[], [@wr], []]
+
+ @wr.write("foobar")
+ # CRuby on macOS returns [[@rd], [@rd], []], weird but we accept it here, probably only for pipe read-end
+ [
+ [[@rd], [], []],
+ [[@rd], [@rd], []]
+ ].should.include? IO.select([@rd], [@rd], [])
+ end
+ end
end
describe "IO.select when passed nil for timeout" do
diff --git a/spec/ruby/core/io/set_encoding_by_bom_spec.rb b/spec/ruby/core/io/set_encoding_by_bom_spec.rb
index 7368ec7677..92433d6640 100644
--- a/spec/ruby/core/io/set_encoding_by_bom_spec.rb
+++ b/spec/ruby/core/io/set_encoding_by_bom_spec.rb
@@ -12,66 +12,251 @@ describe "IO#set_encoding_by_bom" do
rm_r @name
end
- ruby_version_is "2.7" do
- it "returns the result encoding if found BOM UTF-8 sequence" do
- File.binwrite(@name, "\u{FEFF}abc")
+ it "returns nil if not readable" do
+ not_readable_io = new_io(@name, 'wb')
- @io.set_encoding_by_bom.should == Encoding::UTF_8
- @io.external_encoding.should == Encoding::UTF_8
- end
+ not_readable_io.set_encoding_by_bom.should be_nil
+ not_readable_io.external_encoding.should == Encoding::ASCII_8BIT
+ ensure
+ not_readable_io.close
+ end
+
+ it "returns the result encoding if found BOM UTF-8 sequence" do
+ File.binwrite(@name, "\u{FEFF}")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_8
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\u{FEFF}abc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_8
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns the result encoding if found BOM UTF_16LE sequence" do
+ File.binwrite(@name, "\xFF\xFE")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\xFF\xFEabc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns the result encoding if found BOM UTF_16BE sequence" do
+ File.binwrite(@name, "\xFE\xFF")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16BE
+ @io.external_encoding.should == Encoding::UTF_16BE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\xFE\xFFabc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16BE
+ @io.external_encoding.should == Encoding::UTF_16BE
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns the result encoding if found BOM UTF_32LE sequence" do
+ File.binwrite(@name, "\xFF\xFE\x00\x00")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32LE
+ @io.external_encoding.should == Encoding::UTF_32LE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\xFF\xFE\x00\x00abc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32LE
+ @io.external_encoding.should == Encoding::UTF_32LE
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns the result encoding if found BOM UTF_32BE sequence" do
+ File.binwrite(@name, "\x00\x00\xFE\xFF")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32BE
+ @io.external_encoding.should == Encoding::UTF_32BE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\x00\x00\xFE\xFFabc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32BE
+ @io.external_encoding.should == Encoding::UTF_32BE
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns nil if io is empty" do
+ @io.set_encoding_by_bom.should be_nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ end
+
+ it "returns nil if UTF-8 BOM sequence is incomplete" do
+ File.write(@name, "\xEF")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEF".b
+ @io.rewind
+
+ File.write(@name, "\xEFa")
- it "returns the result encoding if found BOM UTF_16LE sequence" do
- File.binwrite(@name, "\xFF\xFEabc")
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEFa".b
+ @io.rewind
- @io.set_encoding_by_bom.should == Encoding::UTF_16LE
- @io.external_encoding.should == Encoding::UTF_16LE
- end
+ File.write(@name, "\xEF\xBB")
- it "returns the result encoding if found BOM UTF_16BE sequence" do
- File.binwrite(@name, "\xFE\xFFabc")
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEF\xBB".b
+ @io.rewind
- @io.set_encoding_by_bom.should == Encoding::UTF_16BE
- @io.external_encoding.should == Encoding::UTF_16BE
- end
+ File.write(@name, "\xEF\xBBa")
- it "returns the result encoding if found BOM UTF_32LE sequence" do
- File.binwrite(@name, "\xFF\xFE\x00\x00abc")
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEF\xBBa".b
+ end
+
+ it "returns nil if UTF-16BE BOM sequence is incomplete" do
+ File.write(@name, "\xFE")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFE".b
+ @io.rewind
+
+ File.write(@name, "\xFEa")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFEa".b
+ end
+
+ it "returns nil if UTF-16LE/UTF-32LE BOM sequence is incomplete" do
+ File.write(@name, "\xFF")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFF".b
+ @io.rewind
- @io.set_encoding_by_bom.should == Encoding::UTF_32LE
- @io.external_encoding.should == Encoding::UTF_32LE
- end
+ File.write(@name, "\xFFa")
- it "returns the result encoding if found BOM UTF_32BE sequence" do
- File.binwrite(@name, "\x00\x00\xFE\xFFabc")
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFFa".b
+ end
+
+ it "returns UTF-16LE if UTF-32LE BOM sequence is incomplete" do
+ File.write(@name, "\xFF\xFE")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.write(@name, "\xFF\xFE\x00")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "\x00".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.write(@name, "\xFF\xFE\x00a")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "\x00a".b
+ end
- @io.set_encoding_by_bom.should == Encoding::UTF_32BE
- @io.external_encoding.should == Encoding::UTF_32BE
- end
+ it "returns nil if UTF-32BE BOM sequence is incomplete" do
+ File.write(@name, "\x00")
- it "returns nil if found BOM sequence not provided" do
- File.write(@name, "abc")
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00".b
+ @io.rewind
- @io.set_encoding_by_bom.should == nil
- end
+ File.write(@name, "\x00a")
- it 'returns exception if io not in binary mode' do
- not_binary_io = new_io(@name, 'r')
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00a".b
+ @io.rewind
- -> { not_binary_io.set_encoding_by_bom }.should raise_error(ArgumentError, 'ASCII incompatible encoding needs binmode')
- ensure
- not_binary_io.close
- end
+ File.write(@name, "\x00\x00")
- it 'returns exception if encoding already set' do
- @io.set_encoding("utf-8")
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00".b
+ @io.rewind
- -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding is set to UTF-8 already')
- end
+ File.write(@name, "\x00\x00a")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00a".b
+ @io.rewind
+
+ File.write(@name, "\x00\x00\xFE")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00\xFE".b
+ @io.rewind
+
+ File.write(@name, "\x00\x00\xFEa")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00\xFEa".b
+ end
+
+ it "returns nil if found BOM sequence not provided" do
+ File.write(@name, "abc")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read(3).should == "abc".b
+ end
+
+ it 'returns exception if io not in binary mode' do
+ not_binary_io = new_io(@name, 'r')
+
+ -> { not_binary_io.set_encoding_by_bom }.should raise_error(ArgumentError, 'ASCII incompatible encoding needs binmode')
+ ensure
+ not_binary_io.close
+ end
+
+ it 'returns exception if encoding already set' do
+ @io.set_encoding("utf-8")
+
+ -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding is set to UTF-8 already')
+ end
- it 'returns exception if encoding conversion is already set' do
- @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE)
+ it 'returns exception if encoding conversion is already set' do
+ @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE)
- -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding conversion is set')
- end
+ -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding conversion is set')
end
end
diff --git a/spec/ruby/core/io/set_encoding_spec.rb b/spec/ruby/core/io/set_encoding_spec.rb
index 5aec6a96c3..22d9017635 100644
--- a/spec/ruby/core/io/set_encoding_spec.rb
+++ b/spec/ruby/core/io/set_encoding_spec.rb
@@ -1,7 +1,7 @@
require_relative '../../spec_helper'
describe :io_set_encoding_write, shared: true do
- it "sets the encodings to nil" do
+ it "sets the encodings to nil when they were set previously" do
@io = new_io @name, "#{@object}:ibm437:ibm866"
@io.set_encoding nil, nil
@@ -9,6 +9,19 @@ describe :io_set_encoding_write, shared: true do
@io.internal_encoding.should be_nil
end
+ it "sets the encodings to nil when the IO is built with no explicit encoding" do
+ @io = new_io @name, @object
+
+ # Checking our assumptions first
+ @io.external_encoding.should be_nil
+ @io.internal_encoding.should be_nil
+
+ @io.set_encoding nil, nil
+
+ @io.external_encoding.should be_nil
+ @io.internal_encoding.should be_nil
+ end
+
it "prevents the encodings from changing when Encoding defaults are changed" do
@io = new_io @name, "#{@object}:utf-8:us-ascii"
@io.set_encoding nil, nil
@@ -38,6 +51,7 @@ describe "IO#set_encoding when passed nil, nil" do
@external = Encoding.default_external
@internal = Encoding.default_internal
+ # The defaults
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = nil
@@ -113,6 +127,22 @@ describe "IO#set_encoding when passed nil, nil" do
describe "with 'a+' mode" do
it_behaves_like :io_set_encoding_write, nil, "a+"
end
+
+ describe "with standard IOs" do
+ it "correctly resets them" do
+ STDOUT.external_encoding.should == nil
+ STDOUT.internal_encoding.should == nil
+
+ begin
+ STDOUT.set_encoding(Encoding::US_ASCII, Encoding::ISO_8859_1)
+ ensure
+ STDOUT.set_encoding(nil, nil)
+ end
+
+ STDOUT.external_encoding.should == nil
+ STDOUT.internal_encoding.should == nil
+ end
+ end
end
describe "IO#set_encoding" do
@@ -188,4 +218,21 @@ describe "IO#set_encoding" do
@io.external_encoding.should == Encoding::UTF_8
@io.internal_encoding.should == Encoding::UTF_16BE
end
+
+ it "saves encoding options passed as a hash in the last argument" do
+ File.write(@name, "\xff")
+ io = File.open(@name)
+ io.set_encoding(Encoding::EUC_JP, Encoding::SHIFT_JIS, invalid: :replace, replace: ".")
+ io.read.should == "."
+ ensure
+ io.close
+ end
+
+ it "raises ArgumentError when no arguments are given" do
+ -> { @io.set_encoding() }.should raise_error(ArgumentError)
+ end
+
+ it "raises ArgumentError when too many arguments are given" do
+ -> { @io.set_encoding(1, 2, 3) }.should raise_error(ArgumentError)
+ end
end
diff --git a/spec/ruby/core/io/shared/binwrite.rb b/spec/ruby/core/io/shared/binwrite.rb
index 3649bb47ff..e51093329b 100644
--- a/spec/ruby/core/io/shared/binwrite.rb
+++ b/spec/ruby/core/io/shared/binwrite.rb
@@ -21,6 +21,14 @@ describe :io_binwrite, shared: true do
IO.send(@method, @filename, "abcde").should == 5
end
+ it "accepts options as a keyword argument" do
+ IO.send(@method, @filename, "hi", 0, flags: File::CREAT).should == 2
+
+ -> {
+ IO.send(@method, @filename, "hi", 0, {flags: File::CREAT})
+ }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 2..3)")
+ end
+
it "creates a file if missing" do
fn = @filename + "xxx"
begin
@@ -67,6 +75,11 @@ describe :io_binwrite, shared: true do
File.read(@filename).should == "\0\0foo"
end
+ it "accepts a :flags option without :mode one" do
+ IO.send(@method, @filename, "hello, world!", flags: File::CREAT)
+ File.read(@filename).should == "hello, world!"
+ end
+
it "raises an error if readonly mode is specified" do
-> { IO.send(@method, @filename, "abcde", mode: "r") }.should raise_error(IOError)
end
diff --git a/spec/ruby/core/io/shared/each.rb b/spec/ruby/core/io/shared/each.rb
index 91766fbe03..aca622834f 100644
--- a/spec/ruby/core/io/shared/each.rb
+++ b/spec/ruby/core/io/shared/each.rb
@@ -33,10 +33,6 @@ describe :io_each, shared: true do
$_.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
-> { IOSpecs.closed_io.send(@method) {} }.should raise_error(IOError)
end
@@ -77,6 +73,10 @@ describe :io_each, shared: true do
-> { @io.send(@method, 0){} }.should raise_error(ArgumentError)
end
end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { @io.send(@method, 2**128){} }.should raise_error(RangeError)
+ end
end
describe "when passed a String containing one space as a separator" do
@@ -113,6 +113,13 @@ describe :io_each, shared: true do
@io.send(@method, "") { |s| ScratchPad << s }
ScratchPad.recorded.should == IOSpecs.paragraphs
end
+
+ it "discards leading newlines" do
+ @io.readline
+ @io.readline
+ @io.send(@method, "") { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1]
+ end
end
describe "with both separator and limit" do
@@ -152,6 +159,13 @@ describe :io_each, shared: true do
@io.send(@method, "", 1024) { |s| ScratchPad << s }
ScratchPad.recorded.should == IOSpecs.paragraphs
end
+
+ it "discards leading newlines" do
+ @io.readline
+ @io.readline
+ @io.send(@method, "", 1024) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1]
+ end
end
end
end
@@ -161,6 +175,70 @@ describe :io_each, shared: true do
@io.send(@method, chomp: true) { |s| ScratchPad << s }
ScratchPad.recorded.should == IOSpecs.lines_without_newline_characters
end
+
+ it "raises exception when options passed as Hash" do
+ -> {
+ @io.send(@method, { chomp: true }) { |s| }
+ }.should raise_error(TypeError)
+
+ -> {
+ @io.send(@method, "\n", 1, { chomp: true }) { |s| }
+ }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
+ end
+
+ describe "when passed chomp and a separator" do
+ it "yields each line without separator to the passed block" do
+ @io.send(@method, " ", chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.lines_space_separator_without_trailing_spaces
+ end
+ end
+
+ describe "when passed chomp and empty line as a separator" do
+ it "yields each paragraph without trailing new line characters" do
+ @io.send(@method, "", 1024, chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs_without_trailing_new_line_characters
+ end
+ end
+
+ describe "when passed chomp and nil as a separator" do
+ ruby_version_is "3.2" do
+ it "yields self's content" do
+ @io.pos = 100
+ @io.send(@method, nil, chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"]
+ end
+ end
+
+ ruby_version_is ""..."3.2" do
+ it "yields self's content without trailing new line character" do
+ @io.pos = 100
+ @io.send(@method, nil, chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six."]
+ end
+ end
+ end
+
+ describe "when passed chomp, nil as a separator, and a limit" do
+ it "yields each line of limit size without truncating trailing new line character" do
+ # 43 - is a size of the 1st paragraph in the file
+ @io.send(@method, nil, 43, chomp: true) { |s| ScratchPad << s }
+
+ ScratchPad.recorded.should == [
+ "Voici la ligne une.\nQui è la linea due.\n\n\n",
+ "Aquí está la línea tres.\n" + "Hier ist Zeile ",
+ "vier.\n\nEstá aqui a linha cinco.\nHere is li",
+ "ne six.\n"
+ ]
+ end
+ end
+
+ describe "when passed too many arguments" do
+ it "raises ArgumentError" do
+ -> {
+ @io.send(@method, "", 1, "excess argument", chomp: true) {}
+ }.should raise_error(ArgumentError)
+ end
end
end
diff --git a/spec/ruby/core/io/shared/new.rb b/spec/ruby/core/io/shared/new.rb
index f2a0970a40..cba5f33ebf 100644
--- a/spec/ruby/core/io/shared/new.rb
+++ b/spec/ruby/core/io/shared/new.rb
@@ -1,5 +1,7 @@
require_relative '../fixtures/classes'
+# NOTE: should be synchronized with library/stringio/initialize_spec.rb
+
# This group of specs may ONLY contain specs that do successfully create
# an IO instance from the file descriptor returned by #new_fd helper.
describe :io_new, shared: true do
@@ -62,6 +64,15 @@ describe :io_new, shared: true do
@io.should be_an_instance_of(IO)
end
+ it "accepts options as keyword arguments" do
+ @io = IO.send(@method, @fd, "w", flags: File::CREAT)
+ @io.write("foo").should == 3
+
+ -> {
+ IO.send(@method, @fd, "w", {flags: File::CREAT})
+ }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 1..2)")
+ end
+
it "accepts a :mode option" do
@io = IO.send(@method, @fd, mode: "w")
@io.write("foo").should == 3
@@ -197,21 +208,10 @@ describe :io_new, shared: true do
@io.internal_encoding.to_s.should == 'IBM866'
end
- ruby_version_is ''...'3.0' do
- it "accepts nil options" do
- @io = suppress_keyword_warning do
- IO.send(@method, @fd, 'w', nil)
- end
- @io.write("foo").should == 3
- end
- end
-
- ruby_version_is '3.0' do
- it "raises ArgumentError for nil options" do
- -> {
- IO.send(@method, @fd, 'w', nil)
- }.should raise_error(ArgumentError)
- end
+ it "raises ArgumentError for nil options" do
+ -> {
+ IO.send(@method, @fd, 'w', nil)
+ }.should raise_error(ArgumentError)
end
it "coerces mode with #to_str" do
@@ -382,21 +382,9 @@ describe :io_new_errors, shared: true do
}.should raise_error(ArgumentError)
end
- ruby_version_is ''...'3.0' do
- it "raises TypeError if passed a hash for mode and nil for options" do
- -> {
- suppress_keyword_warning do
- @io = IO.send(@method, @fd, {mode: 'w'}, nil)
- end
- }.should raise_error(TypeError)
- end
- end
-
- ruby_version_is '3.0' do
- it "raises ArgumentError if passed a hash for mode and nil for options" do
- -> {
- @io = IO.send(@method, @fd, {mode: 'w'}, nil)
- }.should raise_error(ArgumentError)
- end
+ it "raises ArgumentError if passed a hash for mode and nil for options" do
+ -> {
+ @io = IO.send(@method, @fd, {mode: 'w'}, nil)
+ }.should raise_error(ArgumentError)
end
end
diff --git a/spec/ruby/core/io/shared/pos.rb b/spec/ruby/core/io/shared/pos.rb
index d83a6c6692..3fdd3eb2b3 100644
--- a/spec/ruby/core/io/shared/pos.rb
+++ b/spec/ruby/core/io/shared/pos.rb
@@ -60,7 +60,13 @@ describe :io_set_pos, shared: true do
end
end
- it "does not accept Integers that don't fit in a C long" do
+ it "raises TypeError when cannot convert implicitly argument to Integer" do
+ File.open @fname do |io|
+ -> { io.send @method, Object.new }.should raise_error(TypeError, "no implicit conversion of Object into Integer")
+ end
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
File.open @fname do |io|
-> { io.send @method, 2**128 }.should raise_error(RangeError)
end
diff --git a/spec/ruby/core/io/shared/readlines.rb b/spec/ruby/core/io/shared/readlines.rb
index 8e4a73bb98..6c1fa11a59 100644
--- a/spec/ruby/core/io/shared/readlines.rb
+++ b/spec/ruby/core/io/shared/readlines.rb
@@ -73,6 +73,23 @@ describe :io_readlines_options_19, shared: true do
result = IO.send(@method, @name, 10, &@object)
(result ? result : ScratchPad.recorded).should == IOSpecs.lines_limit
end
+
+ it "ignores the object as a limit if it is negative" do
+ result = IO.send(@method, @name, -2, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { IO.send(@method, @name, 2**128, &@object) }.should raise_error(RangeError)
+ end
+
+ ruby_bug "#18767", ""..."3.3" do
+ describe "when passed limit" do
+ it "raises ArgumentError when passed 0 as a limit" do
+ -> { IO.send(@method, @name, 0, &@object) }.should raise_error(ArgumentError)
+ end
+ end
+ end
end
describe "when the object is a String" do
@@ -82,36 +99,38 @@ describe :io_readlines_options_19, shared: true do
end
it "accepts non-ASCII data as separator" do
- result = IO.send(@method, @name, "\303\250".force_encoding("utf-8"), &@object)
+ result = IO.send(@method, @name, "\303\250".dup.force_encoding("utf-8"), &@object)
(result ? result : ScratchPad.recorded).should == IOSpecs.lines_arbitrary_separator
end
end
- describe "when the object is 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
+ describe "when the object is an options Hash" do
+ it "raises TypeError exception" do
+ -> {
+ IO.send(@method, @name, { chomp: true }, &@object)
+ }.should raise_error(TypeError)
end
end
- end
- describe "when passed name, object, object" do
- describe "when the first object is an Integer" do
- it "uses the second object as an options Hash" do
- -> do
- IO.send(@method, @filename, 10, mode: "w", &@object)
- end.should raise_error(IOError)
- end
+ describe "when the object is neither Integer nor String" do
+ it "raises TypeError exception" do
+ obj = mock("not io readlines limit")
- 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" })
- -> do
- IO.send(@method, @filename, 10, **options, &@object)
- end.should raise_error(IOError)
+ -> {
+ IO.send(@method, @name, obj, &@object)
+ }.should raise_error(TypeError)
end
end
+ end
+
+ describe "when passed name, keyword arguments" do
+ it "uses the keyword arguments as options" do
+ result = IO.send(@method, @name, mode: "r", &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines
+ end
+ end
+ describe "when passed name, object, object" do
describe "when the first object is a String" do
it "uses the second object as a limit if it is an Integer" do
result = IO.send(@method, @name, " ", 10, &@object)
@@ -124,32 +143,18 @@ describe :io_readlines_options_19, shared: true do
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
- -> 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" })
- -> do
- IO.send(@method, @filename, " ", **options, &@object)
- end.should raise_error(IOError)
- end
end
describe "when the first object is not a String or Integer" do
it "calls #to_str to convert the object to a String" do
sep = mock("io readlines separator")
sep.should_receive(:to_str).at_least(1).and_return(" ")
- result = IO.send(@method, @name, sep, 10, mode: "r", &@object)
+ result = IO.send(@method, @name, sep, 10, &@object)
(result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
end
it "uses the second object as a limit if it is an Integer" do
- result = IO.send(@method, @name, " ", 10, mode: "r", &@object)
+ result = IO.send(@method, @name, " ", 10, &@object)
(result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
end
@@ -159,24 +164,57 @@ describe :io_readlines_options_19, shared: true do
result = IO.send(@method, @name, " ", limit, &@object)
(result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
end
+ end
+
+ describe "when the second object is neither Integer nor String" do
+ it "raises TypeError exception" do
+ obj = mock("not io readlines limit")
+
+ -> {
+ IO.send(@method, @name, " ", obj, &@object)
+ }.should raise_error(TypeError)
+ end
+ end
- it "uses the second object as an options Hash" do
+ describe "when the second object is an options Hash" do
+ it "raises TypeError exception" do
+ -> {
+ IO.send(@method, @name, "", { chomp: true }, &@object)
+ }.should raise_error(TypeError)
+ end
+ end
+ end
+
+ describe "when passed name, object, keyword arguments" do
+ describe "when the first object is an Integer" do
+ it "uses the keyword arguments as options" do
+ -> do
+ IO.send(@method, @filename, 10, mode: "w", &@object)
+ end.should raise_error(IOError)
+ end
+ end
+
+ describe "when the first object is a String" do
+ it "uses the keyword arguments as options" do
-> do
IO.send(@method, @filename, " ", mode: "w", &@object)
end.should raise_error(IOError)
end
+ end
+
+ describe "when the first object is not a String or Integer" do
+ it "uses the keyword arguments as options" do
+ sep = mock("io readlines separator")
+ sep.should_receive(:to_str).at_least(1).and_return(" ")
- 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" })
-> do
- IO.send(@method, @filename, " ", **options, &@object)
+ IO.send(@method, @filename, sep, mode: "w", &@object)
end.should raise_error(IOError)
end
end
end
- describe "when passed name, separator, limit, options" do
+ describe "when passed name, separator, limit, keyword arguments" do
it "calls #to_path to convert the name object" do
name = mock("io name to_path")
name.should_receive(:to_path).and_return(@name)
@@ -198,12 +236,24 @@ describe :io_readlines_options_19, shared: true do
(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" })
+ it "uses the keyword arguments as options" do
-> do
- IO.send(@method, @filename, " ", 10, **options, &@object)
+ IO.send(@method, @filename, " ", 10, mode: "w", &@object)
end.should raise_error(IOError)
end
+
+ describe "when passed chomp, nil as a separator, and a limit" do
+ it "yields each line of limit size without truncating trailing new line character" do
+ # 43 - is a size of the 1st paragraph in the file
+ result = IO.send(@method, @name, nil, 43, chomp: true, &@object)
+
+ (result ? result : ScratchPad.recorded).should == [
+ "Voici la ligne une.\nQui è la linea due.\n\n\n",
+ "Aquí está la línea tres.\n" + "Hier ist Zeile ",
+ "vier.\n\nEstá aqui a linha cinco.\nHere is li",
+ "ne six.\n"
+ ]
+ end
+ end
end
end
diff --git a/spec/ruby/core/io/shared/write.rb b/spec/ruby/core/io/shared/write.rb
index 270b84ac39..964064746a 100644
--- a/spec/ruby/core/io/shared/write.rb
+++ b/spec/ruby/core/io/shared/write.rb
@@ -69,16 +69,6 @@ describe :io_write, shared: true do
-> { IOSpecs.closed_io.send(@method, "hello") }.should raise_error(IOError)
end
- it "does not modify the passed argument" do
- File.open(@filename, "w") do |f|
- f.set_encoding(Encoding::IBM437)
- # A character whose codepoint differs between UTF-8 and IBM437
- f.write "ƒ".freeze
- end
-
- File.binread(@filename).bytes.should == [159]
- end
-
describe "on a pipe" do
before :each do
@r, @w = IO.pipe
@@ -95,11 +85,11 @@ describe :io_write, shared: true do
@r.read.should == "foo"
end
- # [ruby-core:90895] MJIT worker may leave fd open in a forked child.
- # For instance, MJIT creates a worker before @r.close with fork(), @r.close happens,
- # and the MJIT worker keeps the pipe open until the worker execve().
- # TODO: consider acquiring GVL from MJIT worker.
- guard_not -> { defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? } do
+ # [ruby-core:90895] RJIT worker may leave fd open in a forked child.
+ # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens,
+ # and the RJIT worker keeps the pipe open until the worker execve().
+ # TODO: consider acquiring GVL from RJIT worker.
+ guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do
it "raises Errno::EPIPE if the read end is closed and does not die from SIGPIPE" do
@r.close
-> { @w.send(@method, "foo") }.should raise_error(Errno::EPIPE, /Broken pipe/)
@@ -107,3 +97,58 @@ describe :io_write, shared: true do
end
end
end
+
+describe :io_write_transcode, shared: true do
+ before :each do
+ @transcode_filename = tmp("io_write_transcode")
+ end
+
+ after :each do
+ rm_r @transcode_filename
+ end
+
+ it "transcodes the given string when the external encoding is set and neither is BINARY" do
+ utf8_str = "hello"
+
+ File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
+ file.external_encoding.should == Encoding::UTF_16BE
+ file.send(@method, utf8_str)
+ end
+
+ result = File.binread(@transcode_filename)
+ expected = [0, 104, 0, 101, 0, 108, 0, 108, 0, 111] # UTF-16BE bytes for "hello"
+
+ result.bytes.should == expected
+ end
+
+ it "transcodes the given string when the external encoding is set and the string encoding is BINARY" do
+ str = "été".b
+
+ File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
+ file.external_encoding.should == Encoding::UTF_16BE
+ -> { file.send(@method, str) }.should raise_error(Encoding::UndefinedConversionError)
+ end
+ end
+end
+
+describe :io_write_no_transcode, shared: true do
+ before :each do
+ @transcode_filename = tmp("io_write_no_transcode")
+ end
+
+ after :each do
+ rm_r @transcode_filename
+ end
+
+ it "does not transcode the given string even when the external encoding is set" do
+ utf8_str = "hello"
+
+ File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
+ file.external_encoding.should == Encoding::UTF_16BE
+ file.send(@method, utf8_str)
+ end
+
+ result = File.binread(@transcode_filename)
+ result.bytes.should == utf8_str.bytes
+ end
+end
diff --git a/spec/ruby/core/io/stat_spec.rb b/spec/ruby/core/io/stat_spec.rb
index 58eba02b8f..717c45d0a3 100644
--- a/spec/ruby/core/io/stat_spec.rb
+++ b/spec/ruby/core/io/stat_spec.rb
@@ -3,7 +3,8 @@ require_relative 'fixtures/classes'
describe "IO#stat" do
before :each do
- @io = IO.popen 'cat', "r+"
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ @io = IO.popen cmd, "r+"
end
after :each do
diff --git a/spec/ruby/core/io/sysread_spec.rb b/spec/ruby/core/io/sysread_spec.rb
index 8201ad47ca..003bb9eb94 100644
--- a/spec/ruby/core/io/sysread_spec.rb
+++ b/spec/ruby/core/io/sysread_spec.rb
@@ -6,7 +6,7 @@ describe "IO#sysread on a file" do
@file_name = tmp("IO_sysread_file") + $$.to_s
File.open(@file_name, "w") do |f|
# write some stuff
- f.write("012345678901234567890123456789")
+ f.write("012345678901234567890123456789\nabcdef")
end
@file = File.open(@file_name, "r+")
end
@@ -21,25 +21,25 @@ describe "IO#sysread on a file" do
end
it "reads the specified number of bytes from the file to the buffer" do
- buf = "" # empty buffer
+ buf = +"" # empty buffer
@file.sysread(15, buf).should == buf
buf.should == "012345678901234"
@file.rewind
- buf = "ABCDE" # small buffer
+ buf = +"ABCDE" # small buffer
@file.sysread(15, buf).should == buf
buf.should == "012345678901234"
@file.rewind
- buf = "ABCDE" * 5 # large buffer
+ 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"
+ 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"
@@ -84,6 +84,29 @@ describe "IO#sysread on a file" do
it "raises IOError on closed stream" do
-> { IOSpecs.closed_io.sysread(5) }.should raise_error(IOError)
end
+
+ it "immediately returns an empty string if the length argument is 0" do
+ @file.sysread(0).should == ""
+ end
+
+ it "immediately returns the given buffer if the length argument is 0" do
+ buffer = +"existing content"
+ @file.sysread(0, buffer).should == buffer
+ buffer.should == "existing content"
+ end
+
+ it "discards the existing buffer content upon successful read" do
+ buffer = +"existing content"
+ @file.sysread(11, buffer)
+ buffer.should == "01234567890"
+ end
+
+ it "discards the existing buffer content upon error" do
+ buffer = +"existing content"
+ @file.seek(0, :END)
+ -> { @file.sysread(1, buffer) }.should raise_error(EOFError)
+ buffer.should be_empty
+ end
end
describe "IO#sysread" do
@@ -100,4 +123,10 @@ describe "IO#sysread" do
@write.syswrite "ab"
@read.sysread(3).should == "ab"
end
+
+ guard_not -> { platform_is :windows and ruby_version_is ""..."3.2" } do # https://bugs.ruby-lang.org/issues/18880
+ it "raises ArgumentError when length is less than 0" do
+ -> { @read.sysread(-1) }.should raise_error(ArgumentError)
+ end
+ end
end
diff --git a/spec/ruby/core/io/sysseek_spec.rb b/spec/ruby/core/io/sysseek_spec.rb
index e631939bce..002f2a14eb 100644
--- a/spec/ruby/core/io/sysseek_spec.rb
+++ b/spec/ruby/core/io/sysseek_spec.rb
@@ -4,7 +4,7 @@ require_relative 'fixtures/classes'
require_relative 'shared/pos'
describe "IO#sysseek" do
- it_behaves_like :io_set_pos, :seek
+ it_behaves_like :io_set_pos, :sysseek
end
describe "IO#sysseek" do
diff --git a/spec/ruby/core/io/syswrite_spec.rb b/spec/ruby/core/io/syswrite_spec.rb
index a28cc62174..8bf61a27c3 100644
--- a/spec/ruby/core/io/syswrite_spec.rb
+++ b/spec/ruby/core/io/syswrite_spec.rb
@@ -29,6 +29,16 @@ describe "IO#syswrite on a file" do
end
end
+ it "does not modify the passed argument" do
+ File.open(@filename, "w") do |f|
+ f.set_encoding(Encoding::IBM437)
+ # A character whose codepoint differs between UTF-8 and IBM437
+ f.syswrite("ƒ".freeze)
+ end
+
+ File.binread(@filename).bytes.should == [198, 146]
+ end
+
it "warns if called immediately after a buffered IO#write" do
@file.write("abcde")
-> { @file.syswrite("fghij") }.should complain(/syswrite/)
@@ -68,4 +78,5 @@ end
describe "IO#syswrite" do
it_behaves_like :io_write, :syswrite
+ it_behaves_like :io_write_no_transcode, :syswrite
end
diff --git a/spec/ruby/core/io/try_convert_spec.rb b/spec/ruby/core/io/try_convert_spec.rb
index 5fbd10b6fa..a9e99de7aa 100644
--- a/spec/ruby/core/io/try_convert_spec.rb
+++ b/spec/ruby/core/io/try_convert_spec.rb
@@ -38,7 +38,7 @@ describe "IO.try_convert" do
it "raises a TypeError if the object does not return an IO from #to_io" do
obj = mock("io")
obj.should_receive(:to_io).and_return("io")
- -> { IO.try_convert(obj) }.should raise_error(TypeError)
+ -> { IO.try_convert(obj) }.should raise_error(TypeError, "can't convert MockObject to IO (MockObject#to_io gives String)")
end
it "propagates an exception raised by #to_io" do
diff --git a/spec/ruby/core/io/ungetbyte_spec.rb b/spec/ruby/core/io/ungetbyte_spec.rb
index 776707205a..716743a6af 100644
--- a/spec/ruby/core/io/ungetbyte_spec.rb
+++ b/spec/ruby/core/io/ungetbyte_spec.rb
@@ -36,20 +36,10 @@ describe "IO#ungetbyte" do
@io.getbyte.should == 97
end
- ruby_version_is ''...'2.6.1' do
- it "is an RangeError if the integer is not in 8bit" do
- for i in [4095, 0x4f7574206f6620636861722072616e6765] do
- -> { @io.ungetbyte(i) }.should raise_error(RangeError)
- end
- end
- end
-
- ruby_version_is '2.6.1' do
- it "never raises RangeError" do
- for i in [4095, 0x4f7574206f6620636861722072616e67ff] do
- @io.ungetbyte(i).should be_nil
- @io.getbyte.should == 255
- end
+ it "never raises RangeError" do
+ for i in [4095, 0x4f7574206f6620636861722072616e67ff] do
+ @io.ungetbyte(i).should be_nil
+ @io.getbyte.should == 255
end
end
diff --git a/spec/ruby/core/io/ungetc_spec.rb b/spec/ruby/core/io/ungetc_spec.rb
index 41a455c836..47a4e99ebf 100644
--- a/spec/ruby/core/io/ungetc_spec.rb
+++ b/spec/ruby/core/io/ungetc_spec.rb
@@ -103,19 +103,9 @@ describe "IO#ungetc" do
-> { @io.sysread(1) }.should raise_error(IOError)
end
- ruby_version_is ""..."3.0" do
- 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
- end
-
- ruby_version_is "3.0" do
- it "raises TypeError if passed nil" do
- @io.getc.should == ?V
- proc{@io.ungetc(nil)}.should raise_error(TypeError)
- end
+ it "raises TypeError if passed nil" do
+ @io.getc.should == ?V
+ proc{@io.ungetc(nil)}.should raise_error(TypeError)
end
it "puts one or more characters back in the stream" do
diff --git a/spec/ruby/core/io/write_nonblock_spec.rb b/spec/ruby/core/io/write_nonblock_spec.rb
index a8b9ce0a36..c403c864fd 100644
--- a/spec/ruby/core/io/write_nonblock_spec.rb
+++ b/spec/ruby/core/io/write_nonblock_spec.rb
@@ -31,6 +31,16 @@ platform_is_not :windows do
end
end
+ it "does not modify the passed argument" do
+ File.open(@filename, "w") do |f|
+ f.set_encoding(Encoding::IBM437)
+ # A character whose codepoint differs between UTF-8 and IBM437
+ f.write_nonblock("ƒ".freeze)
+ end
+
+ File.binread(@filename).bytes.should == [198, 146]
+ end
+
it "checks if the file is writable if writing zero bytes" do
-> {
@readonly_file.write_nonblock("")
@@ -40,6 +50,7 @@ platform_is_not :windows do
describe "IO#write_nonblock" do
it_behaves_like :io_write, :write_nonblock
+ it_behaves_like :io_write_no_transcode, :write_nonblock
end
end
diff --git a/spec/ruby/core/io/write_spec.rb b/spec/ruby/core/io/write_spec.rb
index 60e66e998f..4a26f8dbaf 100644
--- a/spec/ruby/core/io/write_spec.rb
+++ b/spec/ruby/core/io/write_spec.rb
@@ -24,10 +24,6 @@ describe "IO#write on a file" do
-> { @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
-
before :each do
@external = Encoding.default_external
@internal = Encoding.default_internal
@@ -40,10 +36,44 @@ describe "IO#write on a file" do
Encoding.default_internal = @internal
end
+ it "returns a length of 0 when writing a blank string" do
+ @file.write('').should == 0
+ end
+
+ it "returns a length of 0 when writing blank strings" do
+ @file.write('', '', '').should == 0
+ end
+
+ it "returns a length of 0 when passed no arguments" do
+ @file.write().should == 0
+ end
+
it "returns the number of bytes written" do
@file.write("hellø").should == 6
end
+ it "does not modify the passed argument" do
+ File.open(@filename, "w") do |f|
+ f.set_encoding(Encoding::IBM437)
+ # A character whose codepoint differs between UTF-8 and IBM437
+ f.write("ƒ".freeze)
+ end
+
+ File.binread(@filename).bytes.should == [159]
+ end
+
+ it "does not modify arguments when passed multiple arguments and external encoding not set" do
+ a, b = "a".freeze, "b".freeze
+
+ File.open(@filename, "w") do |f|
+ f.write(a, b)
+ end
+
+ File.binread(@filename).bytes.should == [97, 98]
+ a.encoding.should == Encoding::UTF_8
+ b.encoding.should == Encoding::UTF_8
+ end
+
it "uses the encoding from the given option for non-ascii encoding" do
File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file|
file.write("hi").should == 8
@@ -51,8 +81,25 @@ describe "IO#write on a file" do
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
+ it "uses the encoding from the given option for non-ascii encoding even if in binary mode" do
+ File.open(@filename, "w", encoding: Encoding::UTF_32LE, binmode: true) do |file|
+ file.should.binmode?
+ file.write("hi").should == 8
+ end
+ File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000"
+
+ File.open(@filename, "wb", encoding: Encoding::UTF_32LE) do |file|
+ file.should.binmode?
+ file.write("hi").should == 8
+ end
+ File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000"
+ end
+
+ it "uses the encoding from the given option for non-ascii encoding when multiple arguments passes" do
+ File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file|
+ file.write("h", "i").should == 8
+ end
+ File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000"
end
it "raises a invalid byte sequence error if invalid bytes are being written" do
@@ -72,6 +119,20 @@ describe "IO#write on a file" do
res = "H#{ë}ll#{ö}"
File.binread(@filename).should == res.force_encoding(Encoding::BINARY)
end
+
+ platform_is_not :windows do
+ it "writes binary data if no encoding is given and multiple arguments passed" do
+ File.open(@filename, "w") do |file|
+ file.write("\x87".b, "ą") # 0x87 isn't a valid UTF-8 binary representation of a character
+ end
+ File.binread(@filename).bytes.should == [0x87, 0xC4, 0x85]
+
+ File.open(@filename, "w") do |file|
+ file.write("\x61".encode("utf-32le"), "ą")
+ end
+ File.binread(@filename).bytes.should == [0x61, 0x00, 0x00, 0x00, 0xC4, 0x85]
+ end
+ end
end
describe "IO.write" do
@@ -86,10 +147,38 @@ describe "IO.write" do
File.read(@filename).should == "\0\0hi"
end
+ it "requires mode to be specified in :open_args" do
+ -> {
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true}])
+ }.should raise_error(IOError, "not opened for writing")
+
+ IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true}]).should == 8
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, mode: "w"}]).should == 8
+ end
+
+ it "requires mode to be specified in :open_args even if flags option passed" do
+ -> {
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}])
+ }.should raise_error(IOError, "not opened for writing")
+
+ IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}]).should == 8
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT, mode: "w"}]).should == 8
+ end
+
it "uses the given encoding and returns the number of bytes written" do
IO.write(@filename, 'hi', mode: "w", encoding: Encoding::UTF_32LE).should == 8
end
+ it "raises ArgumentError if encoding is specified in mode parameter and is given as :encoding option" do
+ -> {
+ IO.write(@filename, 'hi', mode: "w:UTF-16LE:UTF-16BE", encoding: Encoding::UTF_32LE)
+ }.should raise_error(ArgumentError, "encoding specified twice")
+
+ -> {
+ IO.write(@filename, 'hi', mode: "w:UTF-16BE", encoding: Encoding::UTF_32LE)
+ }.should raise_error(ArgumentError, "encoding specified twice")
+ end
+
it "writes the file with the permissions in the :perm parameter" do
rm_r @filename
IO.write(@filename, 'write :perm spec', mode: "w", perm: 0o755).should == 16
@@ -114,23 +203,39 @@ describe "IO.write" 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
+ # rb_cloexec_open() is currently missing a retry on EINTR.
+ # @ioquatix is looking into fixing it. Quarantined until it's done.
+ quarantine! do
+ it "writes correctly" do
+ thr = Thread.new do
+ IO.read(@fifo)
+ end
+ begin
+ string = "hi"
+ IO.write(@fifo, string).should == string.length
+ ensure
+ thr.join
+ end
end
end
end
+
+ ruby_version_is "3.3" do
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ -> {
+ -> {
+ IO.write("|cat", "xxx")
+ }.should output_to_fd("xxx")
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
end
end
describe "IO#write" do
it_behaves_like :io_write, :write
+ it_behaves_like :io_write_transcode, :write
it "accepts multiple arguments" do
IO.pipe do |r, w|
@@ -168,3 +273,25 @@ platform_is :windows do
end
end
end
+
+describe "IO#write on STDOUT" do
+ # https://bugs.ruby-lang.org/issues/14413
+ platform_is_not :windows do
+ it "raises SignalException SIGPIPE if the stream is closed instead of Errno::EPIPE like other IOs" do
+ stderr_file = tmp("stderr")
+ begin
+ IO.popen([*ruby_exe, "-e", "loop { puts :ok }"], "r", err: stderr_file) do |io|
+ io.gets.should == "ok\n"
+ io.close
+ end
+ status = $?
+ status.should_not.success?
+ status.should.signaled?
+ Signal.signame(status.termsig).should == 'PIPE'
+ File.read(stderr_file).should.empty?
+ ensure
+ rm_r stderr_file
+ end
+ end
+ end
+end