spec/async/io/stream_spec.rb in async-io-1.22.0 vs spec/async/io/stream_spec.rb in async-io-1.23.0

- old
+ new

@@ -16,205 +16,253 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -require 'async/io/stream' -require 'async/rspec/buffer' +require 'async/io/socket' +require_relative 'generic_examples' +require_relative 'stream_context' + RSpec.describe Async::IO::Stream do - include_context Async::RSpec::Buffer - include_context Async::RSpec::Memory - include_context Async::RSpec::Reactor + # This constant is part of the public interface, but was renamed to `Async::IO::BLOCK_SIZE`. + describe "::BLOCK_SIZE" do + it "should exist and be reasonable" do + expect(Async::IO::Stream::BLOCK_SIZE).to be_between(1024, 1024*32) + end + end - let!(:stream) {Async::IO::Stream.new(buffer)} - let(:io) {stream.io} - - describe '#close_read' do - let(:sockets) {@sockets = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)} - let!(:stream) {Async::IO::Stream.new(sockets.last)} - after(:each) {@sockets&.each(&:close)} + context "socket I/O" do + let(:sockets) do + @sockets = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) + end - it "can close the reading end of the stream" do - stream.close_read - - expect do - stream.read - end.to raise_error(IOError, /not opened for reading/) + after do + @sockets&.each(&:close) end - it "can close the writing end of the stream" do - stream.close_write + let(:io) {sockets.first} + subject {described_class.new(sockets.last)} + + it_should_behave_like Async::IO + + describe '#close_read' do + let(:sockets) do + @sockets = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) + end - expect do - stream.write("Oh no!") - stream.flush - end.to raise_error(IOError, /not opened for writing/) - end - end - - describe '#read' do - it "should read everything" do - io.write "Hello World" - io.seek(0) + after do + @sockets&.each(&:close) + end - expect(io).to receive(:read).and_call_original.twice + subject {described_class.new(sockets.last)} - expect(stream.read).to be == "Hello World" - expect(stream).to be_eof + it "can close the reading end of the stream" do + expect(subject.io).to receive(:close_read).and_call_original + + subject.close_read + + # Ruby <= 2.4 raises an exception even with exception: false + # expect(stream.read).to be_nil + end + + it "can close the writing end of the stream" do + expect(subject.io).to receive(:close_write).and_call_original + + subject.write("Oh yes!") + subject.close_write + + expect do + subject.write("Oh no!") + subject.flush + end.to raise_error(IOError, /not opened for writing/) + end end - it "should read only the amount requested" do - io.write "Hello World" - io.seek(0) + describe '#read_exactly' do + it "can read several bytes" do + io.write("hello\nworld\n") + + expect(subject.read_exactly(4)).to be == 'hell' + end - expect(io).to receive(:read).and_call_original.twice - - expect(stream.read(4)).to be == "Hell" - expect(stream).to_not be_eof - - expect(stream.read(20)).to be == "o World" - expect(stream).to be_eof + it "can raise exception if io is eof" do + io.close + + expect do + subject.read_exactly(4) + end.to raise_error(EOFError) + end end + end + + context "buffered I/O" do + include_context Async::IO::Stream + include_context Async::RSpec::Memory + include_context Async::RSpec::Reactor - context "with large content" do - it "allocates expected amount of bytes" do - io.write("." * 16*1024) + describe '#read' do + it "should read everything" do + io.write "Hello World" io.seek(0) - buffer = nil + expect(subject.io).to receive(:read_nonblock).and_call_original.twice - expect do - # The read buffer is already allocated, and it will be resized to fit the incoming data. It will be swapped with an empty buffer. - buffer = stream.read(16*1024) - end.to limit_allocations.of(String, count: 1, size: 0) + expect(subject.read).to be == "Hello World" + expect(subject).to be_eof + end + + it "should read only the amount requested" do + io.write "Hello World" + io.seek(0) - expect(buffer.size).to be == 16*1024 + expect(subject.io).to receive(:read_nonblock).and_call_original.twice - io.close + expect(subject.read_partial(4)).to be == "Hell" + expect(subject).to_not be_eof + + expect(subject.read_partial(20)).to be == "o World" + expect(subject).to be_eof end + + context "with large content" do + it "allocates expected amount of bytes" do + io.write("." * 16*1024) + io.seek(0) + + buffer = nil + + expect do + # The read buffer is already allocated, and it will be resized to fit the incoming data. It will be swapped with an empty buffer. + buffer = subject.read(16*1024) + end.to limit_allocations.of(String, count: 1, size: 0) + + expect(buffer.size).to be == 16*1024 + end + end end - end - - describe '#read_until' do - it "can read a line" do - io.write("hello\nworld\n") - io.seek(0) - - expect(stream.read_until("\n")).to be == 'hello' - expect(stream.read_until("\n")).to be == 'world' - expect(stream.read_until("\n")).to be_nil - end - context "with 1-byte block size" do - let!(:stream) {Async::IO::Stream.new(buffer, block_size: 1)} - - it "can read a line with a multi-byte pattern" do - io.write("hello\r\nworld\r\n") + describe '#read_until' do + it "can read a line" do + io.write("hello\nworld\n") io.seek(0) - expect(stream.read_until("\r\n")).to be == 'hello' - expect(stream.read_until("\r\n")).to be == 'world' - expect(stream.read_until("\r\n")).to be_nil + expect(subject.read_until("\n")).to be == 'hello' + expect(subject.read_until("\n")).to be == 'world' + expect(subject.read_until("\n")).to be_nil end - end - context "with large content" do - it "allocates expected amount of bytes" do - expect do - stream.read_until("b") - end.to limit_allocations.of(String, size: 0, count: 1) + context "with 1-byte block size" do + subject! {Async::IO::Stream.new(buffer, block_size: 1)} + + it "can read a line with a multi-byte pattern" do + io.write("hello\r\nworld\r\n") + io.seek(0) + + expect(subject.read_until("\r\n")).to be == 'hello' + expect(subject.read_until("\r\n")).to be == 'world' + expect(subject.read_until("\r\n")).to be_nil + end end - end - end - - describe '#flush' do - it "should not call write if write buffer is empty" do - expect(io).to_not receive(:write) - stream.flush + context "with large content" do + it "allocates expected amount of bytes" do + subject + + expect do + subject.read_until("b") + end.to limit_allocations.of(String, size: 0, count: 1) + end + end end - it "should flush underlying data when it exceeds block size" do - expect(io).to receive(:write).and_call_original.once - - stream.block_size.times do - stream.write("!") + describe '#flush' do + it "should not call write if write buffer is empty" do + expect(subject.io).to_not receive(:write) + + subject.flush end - end - end - - describe '#read_partial' do - before(:each) do - io.write "Hello World!" * 1024 - io.seek(0) - end - it "should avoid calling read" do - expect(io).to receive(:read).and_call_original.once - - expect(stream.read_partial(12)).to be == "Hello World!" + it "should flush underlying data when it exceeds block size" do + expect(subject.io).to receive(:write).and_call_original.once + + subject.block_size.times do + subject.write("!") + end + end end - context "with large content" do - it "allocates only the amount required" do - expect do - stream.read(4*1024) - end.to limit_allocations.of(String, count: 2, size: 4*1024+1) + describe '#read_partial' do + before(:each) do + io.write("Hello World!" * 1024) + io.seek(0) end - it "allocates exact number of bytes being read" do - expect do - stream.read(16*1024) - end.to limit_allocations.of(String, count: 1, size: 0) + it "should avoid calling read" do + expect(subject.io).to receive(:read_nonblock).and_call_original.once + + expect(subject.read_partial(12)).to be == "Hello World!" end - it "allocates expected amount of bytes" do - buffer = nil + context "with large content" do + it "allocates only the amount required" do + expect do + subject.read(4*1024) + end.to limit_allocations.of(String, count: 2, size: 4*1024+1) + end - expect do - buffer = stream.read_partial - end.to limit_allocations.of(String, count: 1) + it "allocates exact number of bytes being read" do + expect do + subject.read_partial(16*1024) + end.to limit_allocations.of(String, count: 1, size: 0) + end - expect(buffer.size).to be == stream.block_size + it "allocates expected amount of bytes" do + buffer = nil + + expect do + buffer = subject.read_partial + end.to limit_allocations.of(String, count: 1) + + expect(buffer.size).to be == subject.block_size + end end end - end - - describe '#write' do - it "should read one line" do - expect(io).to receive(:write).and_call_original.once - - stream.write "Hello World\n" - stream.flush - - io.seek(0) - - expect(stream.read).to be == "Hello World\n" + + describe '#write' do + it "should read one line" do + expect(subject.io).to receive(:write).and_call_original.once + + subject.write "Hello World\n" + subject.flush + + io.seek(0) + expect(subject.read).to be == "Hello World\n" + end end - end - - describe '#eof' do - it "should terminate stream" do - expect do - stream.eof! - end.to raise_exception(EOFError) - - expect(stream).to be_eof + + describe '#eof' do + it "should terminate subject" do + expect do + subject.eof! + end.to raise_exception(EOFError) + + expect(subject).to be_eof + end end - end - - describe '#close' do - it 'can be closed even if underlying io is closed' do - io.close - - expect(stream.io).to be_closed - - # Put some data in the write buffer - stream.write "." - - expect do - stream.close - end.to_not raise_exception + + describe '#close' do + it 'can be closed even if underlying io is closed' do + io.close + + expect(subject.io).to be_closed + + # Put some data in the write buffer + subject.write "." + + expect do + subject.close + end.to_not raise_exception + end end end end