From c7dea0b5b1599350e3d2e7e660889c3897542537 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 11 Sep 2023 11:06:42 +1200 Subject: [PATCH] Migrate tests from `rspec` -> `sus`. --- fixtures/async/http/a_protocol.rb | 565 ++++++++++++++++++ fixtures/async/http/body/a_writable_body.rb | 110 ++++ .../async/http/body/a_writable_body.rb | 113 ---- gems.rb | 2 +- test/async/http/body/slowloris.rb | 4 +- test/async/http/body/writable.rb | 4 +- test/async/http/endpoint.rb | 158 +++-- test/async/http/internet.rb | 22 +- test/async/http/internet/instance.rb | 5 +- test/async/http/performance.rb | 89 --- test/async/http/protocol/http10.rb | 6 +- test/async/http/protocol/http11.rb | 113 ++-- test/async/http/protocol/http11/desync.rb | 24 +- test/async/http/protocol/http2.rb | 150 ++--- test/async/http/protocol/shared_examples.rb | 553 ----------------- 15 files changed, 936 insertions(+), 982 deletions(-) create mode 100644 fixtures/async/http/a_protocol.rb create mode 100644 fixtures/async/http/body/a_writable_body.rb delete mode 100644 fixtures/sus/fixtures/async/http/body/a_writable_body.rb delete mode 100755 test/async/http/performance.rb delete mode 100644 test/async/http/protocol/shared_examples.rb diff --git a/fixtures/async/http/a_protocol.rb b/fixtures/async/http/a_protocol.rb new file mode 100644 index 00000000..fdcb9153 --- /dev/null +++ b/fixtures/async/http/a_protocol.rb @@ -0,0 +1,565 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +require 'async' +require 'async/clock' +require 'async/http/client' +require 'async/http/server' +require 'async/http/endpoint' +require 'async/http/body/hijack' +require 'tempfile' + +require 'protocol/http/body/file' + +require 'sus/fixtures/async/http' + +module Async + module HTTP + AProtocol = Sus::Shared("a protocol") do + include Sus::Fixtures::Async::HTTP::ServerContext + + let(:protocol) {subject} + + it "should have valid scheme" do + expect(client.scheme).to be == "http" + end + + with '#close' do + it 'can close the connection' do + Async do |task| + response = client.get("/") + expect(response).to be(:success?) + response.finish + + client.close + + expect(task.children).to be(:empty?) + end.wait + end + end + + with "huge body", timeout: 600 do + let(:body) {::Protocol::HTTP::Body::File.open("/dev/zero", size: 512*1024**2)} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, body] + end + end + + it "client can download data quickly" do + response = client.get("/") + expect(response).to be(:success?) + + data_size = 0 + duration = Async::Clock.measure do + while chunk = response.body.read + data_size += chunk.bytesize + chunk.clear + end + + response.finish + end + + size_mbytes = data_size / 1024**2 + + inform "Data size: #{size_mbytes}MB Duration: #{duration.round(2)}s Throughput: #{(size_mbytes / duration).round(2)}MB/s" + end + end + + with 'buffered body' do + let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])} + let(:response) {::Protocol::HTTP::Response[200, {}, body]} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + response + end + end + + it "response body should be closed" do + expect(body).to receive(:close) + # expect(response).to receive(:close) + + expect(client.get("/", {}).read).to be == "Hello World" + end + end + + with 'empty body' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[204] + end + end + + it 'properly handles no content responses' do + expect(client.get("/", {}).read).to be_nil + end + end + + with 'with trailer' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if trailer = request.headers['trailer'] + expect(request.headers).not.to have_keys('etag') + request.finish + expect(request.headers).to have_keys('etag') + + ::Protocol::HTTP::Response[200, [], "request trailer"] + else + headers = ::Protocol::HTTP::Headers.new + headers.add('trailer', 'etag') + + body = Async::HTTP::Body::Writable.new + + Async do |task| + body.write("response trailer") + task.sleep(0.01) + headers.add('etag', 'abcd') + body.close + end + + ::Protocol::HTTP::Response[200, headers, body] + end + end + end + + it "can send request trailer" do + skip "Protocol does not support trailers!" unless subject.bidirectional? + + headers = ::Protocol::HTTP::Headers.new + headers.add('trailer', 'etag') + body = Async::HTTP::Body::Writable.new + + Async do |task| + body.write("Hello") + task.sleep(0.01) + headers.add('etag', 'abcd') + body.close + end + + response = client.post("/", headers, body) + expect(response.read).to be == "request trailer" + + expect(response).to be(:success?) + end + + it "can receive response trailer" do + skip "Protocol does not support trailers!" unless subject.bidirectional? + + response = client.get("/") + expect(response.headers).to have_keys('trailer') + headers = response.headers + expect(headers).not.to have_keys('etag') + + expect(response.read).to be == "response trailer" + expect(response).to be(:success?) + + # It was sent as a trailer. + expect(headers).to have_keys('etag') + end + end + + with 'with working server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if request.method == 'POST' + # We stream the request body directly to the response. + ::Protocol::HTTP::Response[200, {}, request.body] + elsif request.method == 'GET' + expect(request.body).to be_nil + + ::Protocol::HTTP::Response[200, { + 'remote-address' => request.remote_address.inspect + }, ["#{request.method} #{request.version}"]] + else + ::Protocol::HTTP::Response[200, {}, ["Hello World"]] + end + end + end + + it "should have valid scheme" do + expect(server.scheme).to be == "http" + end + + it "disconnects slow clients" do + response = client.get("/") + response.read + + # We expect this connection to be closed: + connection = response.connection + + reactor.sleep(1.0) + + response = client.get("/") + response.read + + expect(connection).not.to be(:reusable?) + + # client.close + # reactor.sleep(0.1) + # reactor.print_hierarchy + end + + with 'using GET method' do + let(:expected) {"GET #{protocol::VERSION}"} + + it "can handle many simultaneous requests" do + duration = Async::Clock.measure do + 10.times do + tasks = 100.times.collect do + Async do + client.get("/") + end + end + + tasks.each do |task| + response = task.wait + expect(response).to be(:success?) + expect(response.read).to be == expected + end + end + end + + inform "Pool: #{client.pool}" + inform "Duration: #{duration.round(2)}" + end + + with 'with response' do + let(:response) {client.get("/")} + + def after + response.finish + super + end + + it "can finish gracefully" do + expect(response).to be(:success?) + end + + it "is successful" do + expect(response).to be(:success?) + expect(response.read).to be == expected + end + + it "provides content length" do + expect(response.body.length).not.to be_nil + end + + let(:tempfile) {Tempfile.new} + + it "can save to disk" do + response.save(tempfile.path) + expect(tempfile.read).to be == expected + + tempfile.close + end + + it "has remote-address header" do + expect(response.headers['remote-address']).not.to be_nil + end + + it "has protocol version" do + expect(response.version).not.to be_nil + end + end + end + + with 'HEAD' do + let(:response) {client.head("/")} + + it "is successful and without body" do + expect(response).to be(:success?) + expect(response.body).not.to be_nil + expect(response.body).to be(:empty?) + expect(response.body.length).not.to be_nil + expect(response.read).to be_nil + end + end + + with 'POST' do + let(:response) {client.post("/", {}, ["Hello", " ", "World"])} + + def after + response.finish + super + end + + it "is successful" do + expect(response).to be(:success?) + expect(response.read).to be == "Hello World" + expect(client.pool).not.to be(:busy?) + end + + it "can buffer response" do + buffer = response.finish + + expect(buffer.join).to be == "Hello World" + + expect(client.pool).not.to be(:busy?) + end + + it "should not contain content-length response header" do + expect(response.headers).not.to have_keys('content-length') + end + + it "fails gracefully when closing connection" do + client.pool.acquire do |connection| + connection.stream.close + end + end + end + end + + with 'content length' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, [], ["Content Length: #{request.body.length}"]] + end + end + + it "can send push promises" do + response = client.post("/test", [], ["Hello World!"]) + expect(response).to be(:success?) + + expect(response.body.length).to be == 18 + expect(response.read).to be == "Content Length: 12" + end + end + + with 'hijack with nil response' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + nil + end + end + + it "fails with appropriate error" do + response = client.get("/") + + expect(response).to be(:server_failure?) + end + end + + with 'partial hijack' do + let(:content) {"Hello World!"} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| + stream.write content + stream.write content + stream.close + end + end + end + + it "reads hijacked body" do + response = client.get("/") + + expect(response.read).to be == (content*2) + end + end + + with 'body with incorrect length' do + let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, bad_body] + end + end + + it "fails with appropriate error" do + response = client.get("/") + + expect do + response.read + end.to raise_exception(EOFError) + end + end + + with 'streaming server' do + let(:sent_chunks) {[]} + + let(:app) do + chunks = sent_chunks + + ::Protocol::HTTP::Middleware.for do |request| + body = Async::HTTP::Body::Writable.new + + Async::Reactor.run do |task| + 10.times do |i| + chunk = "Chunk #{i}" + chunks << chunk + + body.write chunk + task.sleep 0.25 + end + + body.finish + end + + ::Protocol::HTTP::Response[200, {}, body] + end + end + + it "can cancel response" do + response = client.get("/") + + expect(response.body.read).to be == "Chunk 0" + + response.close + + expect(sent_chunks).to be == ["Chunk 0"] + end + end + + with 'hijack server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if request.hijack? + io = request.hijack! + io.write "HTTP/1.1 200 Okay\r\nContent-Length: 16\r\n\r\nHijack Succeeded" + io.flush + io.close + else + ::Protocol::HTTP::Response[200, {}, ["Hijack Failed"]] + end + end + end + + it "will hijack response if possible" do + response = client.get("/") + + expect(response.read).to be =~ /Hijack/ + end + end + + with 'broken server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + raise RuntimeError.new('simulated failure') + end + end + + it "can't get /" do + expect do + response = client.get("/") + end.to raise_exception(Exception) + end + end + + with 'slow server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + sleep(endpoint.timeout * 2) + ::Protocol::HTTP::Response[200, {}, []] + end + end + + it "can't get /" do + expect do + client.get("/") + end.to raise_exception(Async::TimeoutError) + end + end + + with 'bi-directional streaming' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + # Echo the request body back to the client. + ::Protocol::HTTP::Response[200, {}, request.body] + end + end + + it "can read from request body and write response body simultaneously" do + skip "Protocol does not support bidirectional streaming!" unless subject.bidirectional? + + body = Async::HTTP::Body::Writable.new + + # Ideally, the flow here is as follows: + # 1/ Client writes headers to server. + # 2/ Client starts writing data to server (in async task). + # 3/ Client reads headers from server. + # 4a/ Client reads data from server. + # 4b/ Client finishes sending data to server. + response = client.post(endpoint.path, [], body) + + expect(response).to be(:success?) + + body.write "." + count = 0 + + response.each do |chunk| + if chunk.bytesize > 32 + body.close + else + count += 1 + body.write chunk*2 + Async::Task.current.sleep(0.1) + end + end + + expect(count).to be == 6 + end + end + + with 'multiple client requests' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, [request.path]] + end + end + + def around + current = Console.logger.level + Console.logger.fatal! + + super + ensure + Console.logger.level = current + end + + it "doesn't cancel all requests" do + tasks = [] + task = Async::Task.current + stopped = [] + + 10.times do + tasks << task.async { + begin + loop do + client.get('http://127.0.0.1:8080/a').finish + end + ensure + stopped << 'a' + end + } + end + + 10.times do + tasks << task.async { + begin + loop do + client.get('http://127.0.0.1:8080/b').finish + end + ensure + stopped << 'b' + end + } + end + + tasks.each do |child| + task.sleep 0.01 + child.stop + end + + expect(stopped.sort).to be == stopped + end + end + end + end +end diff --git a/fixtures/async/http/body/a_writable_body.rb b/fixtures/async/http/body/a_writable_body.rb new file mode 100644 index 00000000..e5c42482 --- /dev/null +++ b/fixtures/async/http/body/a_writable_body.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +require 'protocol/http/body/deflate' + +module Async + module HTTP + module Body + AWritableBody = Sus::Shared("a writable body") do + it "can write and read data" do + 3.times do |i| + body.write("Hello World #{i}") + expect(body.read).to be == "Hello World #{i}" + end + end + + it "can buffer data in order" do + 3.times do |i| + body.write("Hello World #{i}") + end + + 3.times do |i| + expect(body.read).to be == "Hello World #{i}" + end + end + + with '#join' do + it "can join chunks" do + 3.times do |i| + body.write("#{i}") + end + + body.close + + expect(body.join).to be == "012" + end + end + + with '#each' do + it "can read all data in order" do + 3.times do |i| + body.write("Hello World #{i}") + end + + body.close + + 3.times do |i| + chunk = body.read + expect(chunk).to be == "Hello World #{i}" + end + end + + it "can propagate failures" do + reactor.async do + expect do + body.each do |chunk| + raise RuntimeError.new("It was too big!") + end + end.to raise_exception(RuntimeError, message: be =~ /big/) + end + + expect{ + body.write("Beep boop") # This will cause a failure. + ::Async::Task.current.yield + body.write("Beep boop") # This will fail. + }.to raise_exception(RuntimeError, message: be =~ /big/) + end + + it "can propagate failures in nested bodies" do + nested = ::Protocol::HTTP::Body::Deflate.for(body) + + reactor.async do + expect do + nested.each do |chunk| + raise RuntimeError.new("It was too big!") + end + end.to raise_exception(RuntimeError, message: be =~ /big/) + end + + expect{ + body.write("Beep boop") # This will cause a failure. + ::Async::Task.current.yield + body.write("Beep boop") # This will fail. + }.to raise_exception(RuntimeError, message: be =~ /big/) + end + + it "will stop after finishing" do + output_task = reactor.async do + body.each do |chunk| + expect(chunk).to be == "Hello World!" + end + end + + body.write("Hello World!") + body.close + + expect(body).not.to be(:empty?) + + ::Async::Task.current.yield + + expect(output_task).to be(:finished?) + expect(body).to be(:empty?) + end + end + end + end + end +end diff --git a/fixtures/sus/fixtures/async/http/body/a_writable_body.rb b/fixtures/sus/fixtures/async/http/body/a_writable_body.rb deleted file mode 100644 index 90e61bfb..00000000 --- a/fixtures/sus/fixtures/async/http/body/a_writable_body.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2023, by Samuel Williams. - -require 'protocol/http/body/deflate' -require 'sus/fixtures' - -module Sus::Fixtures - module Async - module HTTP - module Body - AWritableBody = Sus::Shared("a writable body") do - it "can write and read data" do - 3.times do |i| - body.write("Hello World #{i}") - expect(body.read).to be == "Hello World #{i}" - end - end - - it "can buffer data in order" do - 3.times do |i| - body.write("Hello World #{i}") - end - - 3.times do |i| - expect(body.read).to be == "Hello World #{i}" - end - end - - with '#join' do - it "can join chunks" do - 3.times do |i| - body.write("#{i}") - end - - body.close - - expect(body.join).to be == "012" - end - end - - with '#each' do - it "can read all data in order" do - 3.times do |i| - body.write("Hello World #{i}") - end - - body.close - - 3.times do |i| - chunk = body.read - expect(chunk).to be == "Hello World #{i}" - end - end - - it "can propagate failures" do - reactor.async do - expect do - body.each do |chunk| - raise RuntimeError.new("It was too big!") - end - end.to raise_exception(RuntimeError, message: be =~ /big/) - end - - expect{ - body.write("Beep boop") # This will cause a failure. - ::Async::Task.current.yield - body.write("Beep boop") # This will fail. - }.to raise_exception(RuntimeError, message: be =~ /big/) - end - - it "can propagate failures in nested bodies" do - nested = Protocol::HTTP::Body::Deflate.for(body) - - reactor.async do - expect do - nested.each do |chunk| - raise RuntimeError.new("It was too big!") - end - end.to raise_exception(RuntimeError, message: be =~ /big/) - end - - expect{ - body.write("Beep boop") # This will cause a failure. - ::Async::Task.current.yield - body.write("Beep boop") # This will fail. - }.to raise_exception(RuntimeError, message: be =~ /big/) - end - - it "will stop after finishing" do - output_task = reactor.async do - body.each do |chunk| - expect(chunk).to be == "Hello World!" - end - end - - body.write("Hello World!") - body.close - - expect(body).not.to be(:empty?) - - ::Async::Task.current.yield - - expect(output_task).to be(:finished?) - expect(body).to be(:empty?) - end - end - end - end - end - end -end diff --git a/gems.rb b/gems.rb index 00a4f757..2173f993 100644 --- a/gems.rb +++ b/gems.rb @@ -28,7 +28,7 @@ gem "covered" gem "sus" gem "sus-fixtures-async" - gem "sus-fixtures-async-http", "~> 0.5" + gem "sus-fixtures-async-http", "~> 0.7" gem "sus-fixtures-openssl" gem "bake" diff --git a/test/async/http/body/slowloris.rb b/test/async/http/body/slowloris.rb index 4cec8510..dc3e48be 100644 --- a/test/async/http/body/slowloris.rb +++ b/test/async/http/body/slowloris.rb @@ -6,14 +6,14 @@ require 'async/http/body/slowloris' require 'sus/fixtures/async' -require 'sus/fixtures/async/http/body/a_writable_body' +require 'async/http/body/a_writable_body' describe Async::HTTP::Body::Slowloris do include Sus::Fixtures::Async::ReactorContext let(:body) {subject.new} - it_behaves_like Sus::Fixtures::Async::HTTP::Body::AWritableBody + it_behaves_like Async::HTTP::Body::AWritableBody it "closes body with error if throughput is not maintained" do body.write("Hello World") diff --git a/test/async/http/body/writable.rb b/test/async/http/body/writable.rb index 9399dfbe..9d553a58 100644 --- a/test/async/http/body/writable.rb +++ b/test/async/http/body/writable.rb @@ -6,12 +6,12 @@ require 'async/http/body/slowloris' require 'sus/fixtures/async' -require 'sus/fixtures/async/http/body/a_writable_body' +require 'async/http/body/a_writable_body' describe Async::HTTP::Body::Writable do include Sus::Fixtures::Async::ReactorContext let(:body) {subject.new} - it_behaves_like Sus::Fixtures::Async::HTTP::Body::AWritableBody + it_behaves_like Async::HTTP::Body::AWritableBody end diff --git a/test/async/http/endpoint.rb b/test/async/http/endpoint.rb index b02a6ec8..d14b1464 100644 --- a/test/async/http/endpoint.rb +++ b/test/async/http/endpoint.rb @@ -6,135 +6,169 @@ require 'async/http/endpoint' -RSpec.describe Async::HTTP::Endpoint do +describe Async::HTTP::Endpoint do it "should fail to parse relative url" do - expect{ - described_class.parse("/foo/bar") - }.to raise_error(ArgumentError, /absolute/) + expect do + subject.parse("/foo/bar") + end.to raise_exception(ArgumentError, message: be =~ /absolute/) end - describe '#port' do + with '#port' do let(:url_string) {"https://localhost:9292"} it "extracts port from URL" do endpoint = Async::HTTP::Endpoint.parse(url_string) - expect(endpoint.port).to eq 9292 + expect(endpoint).to have_attributes(port: be == 9292) end it "extracts port from options" do endpoint = Async::HTTP::Endpoint.parse(url_string, port: 9000) - expect(endpoint.port).to eq 9000 + expect(endpoint).to have_attributes(port: be == 9000) end end - describe '#hostname' do + with '#hostname' do describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292") do - it {is_expected.to have_attributes(hostname: '127.0.0.1')} + it 'has correct hostname' do + expect(subject).to have_attributes(hostname: be == '127.0.0.1') + end it "should be connecting to 127.0.0.1" do expect(subject.endpoint).to be_a Async::IO::SSLEndpoint - expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') - expect(subject.endpoint.endpoint).to have_attributes(hostname: '127.0.0.1') + expect(subject.endpoint).to have_attributes(hostname: be == '127.0.0.1') + expect(subject.endpoint.endpoint).to have_attributes(hostname: be == '127.0.0.1') end end describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292", hostname: 'localhost') do - it {is_expected.to have_attributes(hostname: 'localhost')} - it {is_expected.to_not be_localhost} + it 'has correct hostname' do + expect(subject).to have_attributes(hostname: be == 'localhost') + expect(subject).not.to be(:localhost?) + end it "should be connecting to localhost" do expect(subject.endpoint).to be_a Async::IO::SSLEndpoint - expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') - expect(subject.endpoint.endpoint).to have_attributes(hostname: 'localhost') + expect(subject.endpoint).to have_attributes(hostname: be == '127.0.0.1') + expect(subject.endpoint.endpoint).to have_attributes(hostname: be == 'localhost') end end end - describe '.for' do - context Async::HTTP::Endpoint.for("http", "localhost") do - it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/")} - it {is_expected.to_not be_secure} + with '.for' do + describe Async::HTTP::Endpoint.for("http", "localhost") do + it "should have correct attributes" do + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "localhost", + path: be == "/" + ) + + expect(subject).not.to be(:secure?) + end end - context Async::HTTP::Endpoint.for("http", "localhost", "/foo") do - it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/foo")} + describe Async::HTTP::Endpoint.for("http", "localhost", "/foo") do + it "should have correct attributes" do + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "localhost", + path: be == "/foo" + ) + + expect(subject).not.to be(:secure?) + end end end - describe '#secure?' do - subject {Async::HTTP::Endpoint.parse(description)} - - context 'http://localhost' do - it { is_expected.to_not be_secure } + with '#secure?' do + describe Async::HTTP::Endpoint.parse("http://localhost") do + it "should not be secure" do + expect(subject).not.to be(:secure?) + end end - context 'https://localhost' do - it { is_expected.to be_secure } + describe Async::HTTP::Endpoint.parse("https://localhost") do + it "should be secure" do + expect(subject).to be(:secure?) + end end - context 'with scheme: https' do - subject {Async::HTTP::Endpoint.parse("http://localhost", scheme: 'https')} - - it { is_expected.to be_secure } + with 'scheme: https' do + describe Async::HTTP::Endpoint.parse("http://localhost", scheme: 'https') do + it "should be secure" do + expect(subject).to be(:secure?) + end + end end end - describe '#localhost?' do - subject {Async::HTTP::Endpoint.parse(description)} - - context 'http://localhost' do - it { is_expected.to be_localhost } + with '#localhost?' do + describe Async::HTTP::Endpoint.parse("http://localhost") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://hello.localhost' do - it { is_expected.to be_localhost } + describe Async::HTTP::Endpoint.parse("http://hello.localhost") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://localhost.' do - it { is_expected.to be_localhost } + describe Async::HTTP::Endpoint.parse("http://localhost.") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://hello.localhost.' do - it { is_expected.to be_localhost } + describe Async::HTTP::Endpoint.parse("http://hello.localhost.") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://localhost.com' do - it { is_expected.to_not be_localhost } + describe Async::HTTP::Endpoint.parse("http://localhost.com") do + it "should not be localhost" do + expect(subject).not.to be(:localhost?) + end end end - describe '#path' do - it "can normal urls" do - endpoint = Async::HTTP::Endpoint.parse("http://foo.com/bar?baz") - expect(endpoint.path).to be == "/bar?baz" + with '#path' do + describe Async::HTTP::Endpoint.parse("http://foo.com/bar?baz") do + it "should have correct path" do + expect(subject).to have_attributes(path: be == "/bar?baz") + end end - it "can handle websocket urls" do - endpoint = Async::HTTP::Endpoint.parse("wss://foo.com/bar?baz") - expect(endpoint.path).to be == "/bar?baz" + with 'websocket scheme' do + describe Async::HTTP::Endpoint.parse("wss://foo.com/bar?baz") do + it "should have correct path" do + expect(subject).to have_attributes(path: be == "/bar?baz") + end + end end end end -RSpec.describe "http://www.google.com/search" do - let(:endpoint) {Async::HTTP::Endpoint.parse(subject)} - - it "should be valid endpoint" do - expect{endpoint}.to_not raise_error - end - +describe Async::HTTP::Endpoint.parse("http://www.google.com/search") do it "should select the correct protocol" do - expect(endpoint.protocol).to be Async::HTTP::Protocol::HTTP1 + expect(subject.protocol).to be == Async::HTTP::Protocol::HTTP1 end it "should parse the correct hostname" do - expect(endpoint.hostname).to be == "www.google.com" + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "www.google.com", + path: be == "/search" + ) end it "should not be equal if path is different" do other = Async::HTTP::Endpoint.parse('http://www.google.com/search?q=ruby') - expect(endpoint).to_not be_eql other + expect(subject).not.to be == other + expect(subject).not.to be(:eql?, other) end end diff --git a/test/async/http/internet.rb b/test/async/http/internet.rb index 15597b48..ac25f408 100644 --- a/test/async/http/internet.rb +++ b/test/async/http/internet.rb @@ -7,20 +7,18 @@ require 'async/reactor' require 'json' +require 'sus/fixtures/async' -RSpec.describe Async::HTTP::Internet, timeout: 30 do - include_context Async::RSpec::Reactor +describe Async::HTTP::Internet do + include Sus::Fixtures::Async::ReactorContext + let(:internet) {subject.new} let(:headers) {[['accept', '*/*'], ['user-agent', 'async-http']]} - after do - subject.close - end - it "can fetch remote website" do - response = subject.get("https://www.codeotaku.com/index", headers) + response = internet.get("https://www.codeotaku.com/index", headers) - expect(response).to be_success + expect(response).to be(:success?) response.close end @@ -29,10 +27,10 @@ let(:body) {[JSON.dump(sample)]} # This test is increasingly flakey. - xit "can fetch remote json" do - response = subject.post("https://httpbin.org/anything", headers, body) + it "can fetch remote json" do + response = internet.post("https://httpbin.org/anything", headers, body) - expect(response).to be_success - expect{JSON.parse(response.read)}.to_not raise_error + expect(response).to be(:success?) + expect{JSON.parse(response.read)}.not.to raise_exception end end diff --git a/test/async/http/internet/instance.rb b/test/async/http/internet/instance.rb index 846d2028..d254bd59 100644 --- a/test/async/http/internet/instance.rb +++ b/test/async/http/internet/instance.rb @@ -4,12 +4,11 @@ # Copyright, 2021-2023, by Samuel Williams. require 'async/http/internet/instance' -require 'async/reactor' -RSpec.describe Async::HTTP::Internet, timeout: 5 do +describe Async::HTTP::Internet do describe '.instance' do it "returns an internet instance" do - expect(Async::HTTP::Internet.instance).to be_kind_of(Async::HTTP::Internet) + expect(Async::HTTP::Internet.instance).to be_a(Async::HTTP::Internet) end end end diff --git a/test/async/http/performance.rb b/test/async/http/performance.rb deleted file mode 100755 index acb3df2b..00000000 --- a/test/async/http/performance.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2017-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' - -require_relative 'server_context' -require 'async/container' - -require 'etc' - -RSpec.shared_examples_for 'client benchmark' do - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:9294', timeout: 0.8, reuse_port: true)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, []] - end - end - - let(:url) {endpoint.url.to_s} - let(:repeats) {1000} - let(:concurrency) {Etc.nprocessors || 2} - - before do - Sync do - # We bind the endpoint before running the server so that we know incoming connections will be accepted: - @bound_endpoint = Async::IO::SharedEndpoint.bound(endpoint) - end - - # I feel a dedicated class might be better than this hack: - allow(@bound_endpoint).to receive(:protocol).and_return(endpoint.protocol) - allow(@bound_endpoint).to receive(:scheme).and_return(endpoint.scheme) - - @container = Async::Container.new - - GC.disable - - @container.run(count: concurrency) do |instance| - Async do - instance.ready! - server.run - end - end - - @bound_endpoint.close - end - - after do - @container.stop - - GC.enable - end - - it "runs benchmark", timeout: nil do - if ab = `which ab`.chomp! - system(ab, "-k", "-n", (concurrency*repeats).to_s, "-c", concurrency.to_s, url) - end - - if wrk = `which wrk`.chomp! - system(wrk, "-c", concurrency.to_s, "-d", "2", "-t", concurrency.to_s, url) - end - end -end - -RSpec.describe Async::HTTP::Server do - describe Protocol::HTTP::Middleware::Okay do - let(:server) do - Async::HTTP::Server.new( - Protocol::HTTP::Middleware::Okay, - @bound_endpoint - ) - end - - include_examples 'client benchmark' - end - - describe 'multiple chunks' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do - Protocol::HTTP::Response[200, {}, "Hello World".chars] - end - end - - include_examples 'client benchmark' - end -end diff --git a/test/async/http/protocol/http10.rb b/test/async/http/protocol/http10.rb index 7f06eb98..26ae0be4 100644 --- a/test/async/http/protocol/http10.rb +++ b/test/async/http/protocol/http10.rb @@ -4,8 +4,8 @@ # Copyright, 2018-2023, by Samuel Williams. require 'async/http/protocol/http10' -require_relative 'shared_examples' +require 'async/http/a_protocol' -RSpec.describe Async::HTTP::Protocol::HTTP10 do - it_behaves_like Async::HTTP::Protocol +describe Async::HTTP::Protocol::HTTP10 do + it_behaves_like Async::HTTP::AProtocol end diff --git a/test/async/http/protocol/http11.rb b/test/async/http/protocol/http11.rb index d16bf8ba..af2497ff 100755 --- a/test/async/http/protocol/http11.rb +++ b/test/async/http/protocol/http11.rb @@ -6,75 +6,74 @@ # Copyright, 2023, by Thomas Morgan. require 'async/http/protocol/http11' -require_relative 'shared_examples' +require 'async/http/a_protocol' -RSpec.describe Async::HTTP::Protocol::HTTP11 do - it_behaves_like Async::HTTP::Protocol +describe Async::HTTP::Protocol::HTTP11 do + it_behaves_like Async::HTTP::AProtocol - context 'bad requests' do - include_context Async::HTTP::Server + with 'server' do + include Sus::Fixtures::Async::HTTP::ServerContext + let(:protocol) {subject} - around do |example| - current = Console.logger.level - Console.logger.fatal! - - example.run - ensure - Console.logger.level = current - end - - it "should fail cleanly when path is empty" do - response = client.get("") - - expect(response.status).to be == 400 - end - end - - context 'head request' do - include_context Async::HTTP::Server - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, ["Hello", "World"]] - end - end - - it "doesn't reply with body" do - 5.times do - response = client.head("/") + with 'bad requests' do + def around + current = Console.logger.level + Console.logger.fatal! - expect(response).to be_success - expect(response.version).to be == "HTTP/1.1" - expect(response.body).to be_empty + super + ensure + Console.logger.level = current + end + + it "should fail cleanly when path is empty" do + response = client.get("") - response.read + expect(response.status).to be == 400 end end - end - - context 'raw response' do - include_context Async::HTTP::Server - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - peer = request.hijack! - - peer.write( - "#{request.version} 200 It worked!\r\n" + - "connection: close\r\n" + - "\r\n" + - "Hello World!" - ) - peer.close - - nil + with 'head request' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + Protocol::HTTP::Response[200, {}, ["Hello", "World"]] + end + end + + it "doesn't reply with body" do + 5.times do + response = client.head("/") + + expect(response).to be(:success?) + expect(response.version).to be == "HTTP/1.1" + expect(response.body).to be(:empty?) + + response.read + end end end - it "reads raw response" do - response = client.get("/") + with 'raw response' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + peer = request.hijack! + + peer.write( + "#{request.version} 200 It worked!\r\n" + + "connection: close\r\n" + + "\r\n" + + "Hello World!" + ) + peer.close + + nil + end + end - expect(response.read).to be == "Hello World!" + it "reads raw response" do + response = client.get("/") + + expect(response.read).to be == "Hello World!" + end end end end diff --git a/test/async/http/protocol/http11/desync.rb b/test/async/http/protocol/http11/desync.rb index 5e9f7cf2..d31598a6 100644 --- a/test/async/http/protocol/http11/desync.rb +++ b/test/async/http/protocol/http11/desync.rb @@ -3,23 +3,26 @@ # Released under the MIT License. # Copyright, 2021-2023, by Samuel Williams. -require_relative '../../server_context' require 'async/http/protocol/http11' -RSpec.describe Async::HTTP::Protocol::HTTP11, timeout: 30 do - include_context Async::HTTP::Server +require 'sus/fixtures/async/http/server_context' + +describe Async::HTTP::Protocol::HTTP11 do + include Sus::Fixtures::Async::ReactorContext + include Sus::Fixtures::Async::HTTP::ServerContext - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + let(:app) do + Protocol::HTTP::Middleware.for do |request| Protocol::HTTP::Response[200, {}, [request.path]] end end - around do |example| + + def around current = Console.logger.level Console.logger.fatal! - - example.run + + super ensure Console.logger.level = current end @@ -63,7 +66,8 @@ child.stop end - puts "Backtraces" - pp backtraces.sort.uniq + # puts "Backtraces" + # pp backtraces.sort.uniq + expect(backtraces).not.to be(:empty?) end end diff --git a/test/async/http/protocol/http2.rb b/test/async/http/protocol/http2.rb index 2e8a52a8..bad0635c 100644 --- a/test/async/http/protocol/http2.rb +++ b/test/async/http/protocol/http2.rb @@ -4,98 +4,98 @@ # Copyright, 2018-2023, by Samuel Williams. require 'async/http/protocol/http2' -require_relative 'shared_examples' +require 'async/http/a_protocol' -RSpec.describe Async::HTTP::Protocol::HTTP2 do - it_behaves_like Async::HTTP::Protocol +describe Async::HTTP::Protocol::HTTP2 do + it_behaves_like Async::HTTP::AProtocol - context 'bad requests' do - include_context Async::HTTP::Server + with 'server' do + include Sus::Fixtures::Async::HTTP::ServerContext + let(:protocol) {subject} - it "should fail with explicit authority" do - expect do - client.post("/", [[':authority', 'foo']]) - end.to raise_error(Protocol::HTTP2::StreamError) - end - end - - context 'closed streams' do - include_context Async::HTTP::Server - - it 'should delete stream after response stream is closed' do - response = client.get("/") - connection = response.connection - - response.read - - expect(connection.streams).to be_empty + with 'bad requests' do + it "should fail with explicit authority" do + expect do + client.post("/", [[':authority', 'foo']]) + end.to raise_exception(Protocol::HTTP2::StreamError) + end end - end - - context 'host header' do - include_context Async::HTTP::Server - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, request.headers, ["Authority: #{request.authority.inspect}"]] + with 'closed streams' do + it 'should delete stream after response stream is closed' do + response = client.get("/") + connection = response.connection + + response.read + + expect(connection.streams).to be(:empty?) end end - # We specify nil for the authority - it won't be sent. - let!(:client) {Async::HTTP::Client.new(endpoint, authority: nil)} - - it "should not send :authority header if host header is present" do - response = client.post("/", [['host', 'foo']]) + with 'host header' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + Protocol::HTTP::Response[200, request.headers, ["Authority: #{request.authority.inspect}"]] + end + end - expect(response.headers).to include('host') - expect(response.headers['host']).to be == 'foo' + def make_client(endpoint, **options) + # We specify nil for the authority - it won't be sent. + options[:authority] = nil + super + end - # TODO Should HTTP/2 respect host header? - expect(response.read).to be == "Authority: nil" - end - end - - context 'stopping requests' do - include_context Async::HTTP::Server - - let(:notification) {Async::Notification.new} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - body = Async::HTTP::Body::Writable.new + it "should not send :authority header if host header is present" do + response = client.post("/", [['host', 'foo']]) - reactor.async do |task| - begin - 100.times do |i| - body.write("Chunk #{i}") - task.sleep (0.01) - end - rescue - # puts "Response generation failed: #{$!}" - ensure - body.close - notification.signal - end - end + expect(response.headers).to have_keys('host') + expect(response.headers['host']).to be == 'foo' - Protocol::HTTP::Response[200, {}, body] + # TODO Should HTTP/2 respect host header? + expect(response.read).to be == "Authority: nil" end end - let(:pool) {client.pool} - - it "should close stream without closing connection" do - expect(pool).to be_empty - - response = client.get("/") + with 'stopping requests' do + let(:notification) {Async::Notification.new} - expect(pool).to_not be_empty - - response.close + let(:app) do + Protocol::HTTP::Middleware.for do |request| + body = Async::HTTP::Body::Writable.new + + reactor.async do |task| + begin + 100.times do |i| + body.write("Chunk #{i}") + task.sleep (0.01) + end + rescue + # puts "Response generation failed: #{$!}" + ensure + body.close + notification.signal + end + end + + Protocol::HTTP::Response[200, {}, body] + end + end - notification.wait + let(:pool) {client.pool} - expect(response.stream.connection).to be_reusable + it "should close stream without closing connection" do + expect(pool).to be(:empty?) + + response = client.get("/") + + expect(pool).not.to be(:empty?) + + response.close + + notification.wait + + expect(response.stream.connection).to be(:reusable?) + end end end -end +end \ No newline at end of file diff --git a/test/async/http/protocol/shared_examples.rb b/test/async/http/protocol/shared_examples.rb deleted file mode 100644 index b29873d5..00000000 --- a/test/async/http/protocol/shared_examples.rb +++ /dev/null @@ -1,553 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. -# Copyright, 2020, by Igor Sidorov. - -require_relative '../server_context' - -require 'async' -require 'async/clock' -require 'async/http/client' -require 'async/http/server' -require 'async/http/endpoint' -require 'async/http/body/hijack' -require 'tempfile' - -require 'protocol/http/body/file' - -require 'async/rspec/profile' - -RSpec.shared_examples_for Async::HTTP::Protocol do - include_context Async::HTTP::Server - - it "should have valid scheme" do - expect(client.scheme).to be == "http" - end - - context '#close' do - it 'can close the connection' do - Async do |task| - response = client.get("/") - expect(response).to be_success - response.finish - - client.close - - expect(task.children).to be_empty - end.wait - end - end - - context "huge body", timeout: 600 do - let(:body) {Protocol::HTTP::Body::File.open("/dev/zero", size: 512*1024**2)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, body] - end - end - - it "client can download data quickly" do |example| - response = client.get("/") - expect(response).to be_success - - data_size = 0 - duration = Async::Clock.measure do - while chunk = response.body.read - data_size += chunk.bytesize - chunk.clear - end - - response.finish - end - - size_mbytes = data_size / 1024**2 - - example.reporter.message "Data size: #{size_mbytes}MB Duration: #{duration.round(2)}s Throughput: #{(size_mbytes / duration).round(2)}MB/s" - end - end - - context 'buffered body' do - let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])} - let(:response) {Protocol::HTTP::Response[200, {}, body]} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - response - end - end - - it "response body should be closed" do - expect(body).to receive(:close).and_call_original - # expect(response).to receive(:close).and_call_original - - expect(client.get("/", {}).read).to be == "Hello World" - end - end - - context 'empty body' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[204] - end - end - - it 'properly handles no content responses' do - expect(client.get("/", {}).read).to be_nil - end - end - - context 'with trailer', if: described_class.bidirectional? do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if trailer = request.headers['trailer'] - expect(request.headers).to_not include('etag') - request.finish - expect(request.headers).to include('etag') - - Protocol::HTTP::Response[200, [], "request trailer"] - else - headers = Protocol::HTTP::Headers.new - headers.add('trailer', 'etag') - - body = Async::HTTP::Body::Writable.new - - Async do |task| - body.write("response trailer") - task.sleep(0.01) - headers.add('etag', 'abcd') - body.close - end - - Protocol::HTTP::Response[200, headers, body] - end - end - end - - it "can send request trailer" do - headers = Protocol::HTTP::Headers.new - headers.add('trailer', 'etag') - body = Async::HTTP::Body::Writable.new - - Async do |task| - body.write("Hello") - task.sleep(0.01) - headers.add('etag', 'abcd') - body.close - end - - response = client.post("/", headers, body) - expect(response.read).to be == "request trailer" - - expect(response).to be_success - end - - it "can receive response trailer" do - response = client.get("/") - expect(response.headers).to include('trailer') - headers = response.headers - expect(headers).to_not include('etag') - - expect(response.read).to be == "response trailer" - expect(response).to be_success - - # It was sent as a trailer. - expect(headers).to include('etag') - end - end - - context 'with working server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if request.method == 'POST' - # We stream the request body directly to the response. - Protocol::HTTP::Response[200, {}, request.body] - elsif request.method == 'GET' - expect(request.body).to be nil - - Protocol::HTTP::Response[200, { - 'remote-address' => request.remote_address.inspect - }, ["#{request.method} #{request.version}"]] - else - Protocol::HTTP::Response[200, {}, ["Hello World"]] - end - end - end - - it "should have valid scheme" do - expect(server.scheme).to be == "http" - end - - it "disconnects slow clients" do - response = client.get("/") - response.read - - # We expect this connection to be closed: - connection = response.connection - - reactor.sleep(1.0) - - response = client.get("/") - response.read - - expect(connection).to_not be_reusable - - # client.close - # reactor.sleep(0.1) - # reactor.print_hierarchy - end - - context 'using GET method' do - let(:expected) {"GET #{protocol::VERSION}"} - - it "can handle many simultaneous requests", timeout: 10 do |example| - duration = Async::Clock.measure do - 10.times do - tasks = 100.times.collect do - Async do - client.get("/") - end - end - - tasks.each do |task| - response = task.wait - expect(response).to be_success - expect(response.read).to eq expected - end - end - end - - example.reporter.message "Pool: #{client.pool}" - example.reporter.message "Duration = #{duration.round(2)}" - end - - context 'with response' do - let(:response) {client.get("/")} - after {response.finish} - - it "can finish gracefully" do - expect(response).to be_success - end - - it "is successful" do - expect(response).to be_success - expect(response.read).to eq expected - end - - it "provides content length" do - expect(response.body.length).to_not be_nil - end - - let(:tempfile) {Tempfile.new} - - it "can save to disk" do - response.save(tempfile.path) - expect(tempfile.read).to eq expected - - tempfile.close - end - - it "has remote-address header" do - expect(response.headers['remote-address']).to_not be_nil - end - - it "has protocol version" do - expect(response.version).to_not be_nil - end - end - end - - context 'HEAD' do - let(:response) {client.head("/")} - after {response.finish} - - it "is successful and without body" do - expect(response).to be_success - expect(response.body).to_not be_nil - expect(response.body).to be_empty - expect(response.body.length).to_not be_nil - expect(response.read).to be_nil - end - end - - context 'POST' do - let(:response) {client.post("/", {}, ["Hello", " ", "World"])} - - after {response.finish} - - it "is successful" do - expect(response).to be_success - expect(response.read).to be == "Hello World" - - expect(client.pool).to_not be_busy - end - - it "can buffer response" do - buffer = response.finish - - expect(buffer.join).to be == "Hello World" - - expect(client.pool).to_not be_busy - end - - it "should not contain content-length response header" do - expect(response.headers).to_not include('content-length') - end - - it "fails gracefully when closing connection" do - client.pool.acquire do |connection| - connection.stream.close - end - end - end - end - - context 'content length' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, [], ["Content Length: #{request.body.length}"]] - end - end - - it "can send push promises" do - response = client.post("/test", [], ["Hello World!"]) - expect(response).to be_success - - expect(response.body.length).to be == 18 - expect(response.read).to be == "Content Length: 12" - end - end - - context 'hijack with nil response' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - nil - end - end - - it "fails with appropriate error" do - response = client.get("/") - - expect(response).to be_server_failure - end - end - - context 'partial hijack' do - let(:content) {"Hello World!"} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| - stream.write content - stream.write content - stream.close - end - end - end - - it "reads hijacked body" do - response = client.get("/") - - expect(response.read).to be == (content*2) - end - end - - context 'body with incorrect length' do - let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, bad_body] - end - end - - it "fails with appropriate error" do - response = client.get("/") - - expect do - response.read - end.to raise_error(EOFError) - end - end - - context 'streaming server' do - let!(:sent_chunks) {[]} - - let(:server) do - chunks = sent_chunks - - Async::HTTP::Server.for(@bound_endpoint) do |request| - body = Async::HTTP::Body::Writable.new - - Async::Reactor.run do |task| - 10.times do |i| - chunk = "Chunk #{i}" - chunks << chunk - - body.write chunk - task.sleep 0.25 - end - - body.finish - end - - Protocol::HTTP::Response[200, {}, body] - end - end - - it "can cancel response" do - response = client.get("/") - - expect(response.body.read).to be == "Chunk 0" - - response.close - - expect(sent_chunks).to be == ["Chunk 0"] - end - end - - context 'hijack server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if request.hijack? - io = request.hijack! - io.write "HTTP/1.1 200 Okay\r\nContent-Length: 16\r\n\r\nHijack Succeeded" - io.flush - io.close - else - Protocol::HTTP::Response[200, {}, ["Hijack Failed"]] - end - end - end - - it "will hijack response if possible" do - response = client.get("/") - - expect(response.read).to include("Hijack") - end - end - - context 'broken server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - raise RuntimeError.new('simulated failure') - end - end - - it "can't get /" do - expect do - response = client.get("/") - end.to raise_error(Exception) - end - end - - context 'slow server' do - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:0', reuse_port: true, timeout: 0.1)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::Task.current.sleep(endpoint.timeout * 2) - Protocol::HTTP::Response[200, {}, []] - end - end - - it "can't get /" do - expect do - client.get("/") - end.to raise_error(Async::TimeoutError) - end - end - - context 'bi-directional streaming', if: described_class.bidirectional? do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - # Echo the request body back to the client. - Protocol::HTTP::Response[200, {}, request.body] - end - end - - it "can read from request body and write response body simultaneously" do - body = Async::HTTP::Body::Writable.new - - # Ideally, the flow here is as follows: - # 1/ Client writes headers to server. - # 2/ Client starts writing data to server (in async task). - # 3/ Client reads headers from server. - # 4a/ Client reads data from server. - # 4b/ Client finishes sending data to server. - response = client.post(endpoint.path, [], body) - - expect(response).to be_success - - body.write "." - count = 0 - - response.each do |chunk| - if chunk.bytesize > 32 - body.close - else - count += 1 - body.write chunk*2 - Async::Task.current.sleep(0.1) - end - end - - expect(count).to be == 6 - end - end - - context 'multiple client requests' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, [request.path]] - end - end - - around do |example| - current = Console.logger.level - Console.logger.fatal! - - example.run - ensure - Console.logger.level = current - end - - it "doesn't cancel all requests" do - tasks = [] - task = Async::Task.current - stopped = [] - - 10.times do - tasks << task.async { - begin - loop do - client.get('http://127.0.0.1:8080/a').finish - end - ensure - stopped << 'a' - end - } - end - - 10.times do - tasks << task.async { - begin - loop do - client.get('http://127.0.0.1:8080/b').finish - end - ensure - stopped << 'b' - end - } - end - - tasks.each do |child| - task.sleep 0.01 - child.stop - end - - expect(stopped.sort).to be == stopped - end - end -end