diff --git a/async-http.gemspec b/async-http.gemspec index fb012294..90e51d38 100644 --- a/async-http.gemspec +++ b/async-http.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |spec| spec.add_dependency("http-2", "~> 0.9.0") # spec.add_dependency("openssl") - spec.add_development_dependency "async-rspec", "~> 1.1" + spec.add_development_dependency "async-rspec", "~> 1.8" spec.add_development_dependency "async-container", "~> 0.5.0" spec.add_development_dependency "bundler", "~> 1.3" diff --git a/lib/async/http/body/buffered.rb b/lib/async/http/body/buffered.rb index e4a01432..c8d68145 100644 --- a/lib/async/http/body/buffered.rb +++ b/lib/async/http/body/buffered.rb @@ -58,22 +58,13 @@ def length end def empty? - @chunks.empty? + @index >= @chunks.length end def close self end - def each - return to_enum unless block_given? - - while @index < @chunks.count - yield @chunks[@index] - @index += 1 - end - end - def read if chunk = @chunks[@index] @index += 1 diff --git a/spec/async/http/body/buffered_spec.rb b/spec/async/http/body/buffered_spec.rb new file mode 100644 index 00000000..273260b7 --- /dev/null +++ b/spec/async/http/body/buffered_spec.rb @@ -0,0 +1,117 @@ +# Copyright, 2018, by Samuel G. D. Williams. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# 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/http/body/buffered' + +RSpec.describe Async::HTTP::Body::Buffered do + include_context Async::RSpec::Memory + + let(:body) {["Hello", "World"]} + subject! {described_class.wrap(body)} + + describe ".wrap" do + context "when body is a Body::Readable" do + let(:stream) { Async::IO::Stream.new(StringIO.new("content")) } + let(:body) { Async::HTTP::Body::Fixed.new(stream, 7) } + + it "returns the body" do + expect(subject).to be == body + end + end + + context "when body is an Array" do + let(:body) {["Hello", "World"]} + + it "returns instance initialized with the array" do + expect(subject).to be_an_instance_of(described_class) + end + end + + context "when body responds to #each" do + let(:body) {["Hello", "World"].each} + + it "buffers the content into an array before initializing" do + expect(subject).to be_an_instance_of(described_class) + allow(body).to receive(:each).and_raise(StopIteration) + expect(subject.read).to be == "Hello" + expect(subject.read).to be == "World" + end + end + end + + describe "#length" do + it "returns sum of chunks' bytesize" do + expect(subject.length).to be == 10 + end + end + + describe "#empty?" do + it "returns false when there are chunks left" do + expect(subject.empty?).to be == false + subject.read + expect(subject.empty?).to be == false + end + + it "returns true when there are no chunks left" do + subject.read + subject.read + expect(subject.empty?).to be == true + end + + it "returns false when rewinded" do + subject.read + subject.read + subject.rewind + expect(subject.empty?).to be == false + end + end + + describe "#close" do + it "returns self" do + expect(subject.close).to be == subject + end + end + + describe "#read" do + it "retrieves chunks of content" do + expect(subject.read).to be == "Hello" + expect(subject.read).to be == "World" + expect(subject.read).to be == nil + end + + context "with large content" do + let(:content) { Array.new(5) { |i| "#{i}" * (1*1024*1024) } } + + it "allocates expected amount of memory" do + expect do + subject.read until subject.empty? + end.to limit_allocations(size: 0) + end + end + end + + describe "#rewind" do + it "positions the cursor to the beginning" do + expect(subject.read).to be == "Hello" + subject.rewind + expect(subject.read).to be == "Hello" + end + end +end diff --git a/spec/async/http/body/chunked_spec.rb b/spec/async/http/body/chunked_spec.rb new file mode 100644 index 00000000..b3fbb979 --- /dev/null +++ b/spec/async/http/body/chunked_spec.rb @@ -0,0 +1,75 @@ +# Copyright, 2018, by Samuel G. D. Williams. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# 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/http/body/chunked' + +RSpec.describe Async::HTTP::Body::Chunked do + include_context Async::RSpec::Memory + + let(:content) {"Hello World"} + let(:io) {StringIO.new("#{content.bytesize.to_s(16)}\r\n#{content}\r\n0\r\n\r\n")} + let(:stream) {Async::IO::Stream.new(io)} + let(:protocol) {Async::HTTP::Protocol::HTTP11.new(stream)} + subject! {described_class.new(protocol)} + + describe "#empty?" do + it "returns whether EOF was reached" do + expect(subject.empty?).to be == false + end + end + + describe "#stop" do + it "closes the stream" do + subject.stop(:error) + expect(stream).to be_closed + end + + it "marks body as finished" do + subject.stop(:error) + expect(subject).to be_empty + end + end + + describe "#read" do + it "retrieves chunks of content" do + expect(subject.read).to be == "Hello World" + expect(subject.read).to be == nil + expect(subject.read).to be == nil + end + + it "updates number of bytes retrieved" do + subject.read + subject.read # realizes there are no more chunks + expect(subject).to be_empty + end + + context "with large stream" do + let!(:content) { "a" * 5*1024*1024 } + + it "allocates expected amount of memory" do + expect do + while (chunk = subject.read) + chunk.clear + end + end.to limit_allocations(size: 100*1024) + end + end + end +end diff --git a/spec/async/http/body/fixed_spec.rb b/spec/async/http/body/fixed_spec.rb new file mode 100644 index 00000000..ce0d362c --- /dev/null +++ b/spec/async/http/body/fixed_spec.rb @@ -0,0 +1,97 @@ +# Copyright, 2018, by Samuel G. D. Williams. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# 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/http/body/fixed' + +RSpec.describe Async::HTTP::Body::Fixed do + include_context Async::RSpec::Memory + + let(:content) {"Hello World"} + let(:io) {StringIO.new(content)} + let(:stream) {Async::IO::Stream.new(io)} + subject! {described_class.new(stream, io.size)} + + describe "#empty?" do + it "returns whether EOF was reached" do + expect(subject.empty?).to be == false + end + end + + describe "#stop" do + it "closes the stream" do + subject.stop(:error) + expect(stream).to be_closed + end + + it "doesn't close the stream when EOF was reached" do + subject.read + subject.stop(:error) + expect(stream).not_to be_closed + end + end + + describe "#read" do + it "retrieves chunks of content" do + expect(subject.read).to be == "Hello World" + expect(subject.read).to be == nil + end + + it "updates number of bytes retrieved" do + subject.read + expect(subject).to be_empty + end + + context "when provided length is smaller than stream size" do + subject {described_class.new(stream, 5)} + + it "retrieves content up to provided length" do + expect(subject.read).to be == "Hello" + expect(subject.read).to be == nil + end + + it "updates number of bytes retrieved" do + subject.read + expect(subject).to be_empty + end + end + + context "with large stream" do + let(:content) { "a" * 5*1024*1024 } + + it "allocates expected amount of memory" do + expect do + subject.read.clear until subject.empty? + end.to limit_allocations(size: 100*1024) + end + end + end + + describe "#join" do + it "returns all content" do + expect(subject.join).to be == "Hello World" + expect(subject.join).to be == "" + end + + it "updates number of bytes retrieved" do + subject.read + expect(subject).to be_empty + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8b9ddff3..18994c0d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,6 +19,7 @@ require 'bundler/setup' require 'async/http' require 'async/rspec/reactor' +require 'async/rspec/memory' # Async.logger.level = Logger::DEBUG