hatetepe 0.5.2 → 0.6.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/.yardopts +1 -0
- data/Gemfile +9 -4
- data/Gemfile.devtools +55 -0
- data/LICENSE.txt +22 -0
- data/README.md +39 -192
- data/Rakefile +3 -2
- data/bin/hatetepe +35 -2
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +103 -0
- data/config/rubocop.yml +58 -0
- data/config/yardstick.yml +2 -0
- data/hatetepe.gemspec +23 -27
- data/lib/hatetepe/client/keep_alive.rb +59 -0
- data/lib/hatetepe/client/timeouts.rb +19 -0
- data/lib/hatetepe/client.rb +54 -302
- data/lib/hatetepe/connection/eventmachine.rb +61 -0
- data/lib/hatetepe/connection/status.rb +28 -0
- data/lib/hatetepe/errors.rb +7 -0
- data/lib/hatetepe/promise.rb +86 -0
- data/lib/hatetepe/request.rb +15 -39
- data/lib/hatetepe/response.rb +82 -22
- data/lib/hatetepe/serializer/encoding.rb +58 -0
- data/lib/hatetepe/serializer.rb +61 -0
- data/lib/hatetepe/server/keep_alive.rb +53 -13
- data/lib/hatetepe/server/timeouts.rb +17 -0
- data/lib/hatetepe/server.rb +37 -85
- data/lib/hatetepe/support/handlers.rb +19 -0
- data/lib/hatetepe/support/keep_alive.rb +14 -0
- data/lib/hatetepe/support/message.rb +40 -0
- data/lib/hatetepe/version.rb +3 -1
- data/lib/hatetepe.rb +29 -7
- data/spec/integration/error_handling_spec.rb +7 -0
- data/spec/integration/keep_alive_spec.rb +106 -0
- data/spec/integration/smoke_spec.rb +21 -0
- data/spec/integration/streaming_spec.rb +61 -0
- data/spec/integration/timeouts_spec.rb +82 -0
- data/spec/shared/integration/server_client_pair.rb +26 -0
- data/spec/spec_helper.rb +41 -10
- data/spec/support/handler.rb +55 -0
- data/spec/support/helper.rb +74 -0
- data/spec/unit/client_spec.rb +115 -156
- data/spec/unit/connection/eventmachine_spec.rb +146 -0
- data/spec/unit/request_spec.rb +35 -0
- data/spec/unit/response_spec.rb +42 -0
- data/spec/unit/server_spec.rb +65 -100
- data/spec/unit/support/keep_alive_spec.rb +52 -0
- data/spec/unit/support/message_spec.rb +41 -0
- metadata +68 -103
- data/Gemfile.lock +0 -46
- data/LICENSE +0 -19
- data/Procfile +0 -1
- data/config.ru +0 -7
- data/examples/parallel_requests.rb +0 -32
- data/lib/hatetepe/body.rb +0 -182
- data/lib/hatetepe/builder.rb +0 -171
- data/lib/hatetepe/cli.rb +0 -61
- data/lib/hatetepe/connection.rb +0 -73
- data/lib/hatetepe/events.rb +0 -35
- data/lib/hatetepe/message.rb +0 -13
- data/lib/hatetepe/parser.rb +0 -83
- data/lib/hatetepe/server/pipeline.rb +0 -20
- data/lib/hatetepe/server/rack_app.rb +0 -39
- data/lib/rack/handler/hatetepe.rb +0 -33
- data/spec/integration/cli/start_spec.rb +0 -113
- data/spec/integration/client/keep_alive_spec.rb +0 -23
- data/spec/integration/client/timeout_spec.rb +0 -97
- data/spec/integration/server/keep_alive_spec.rb +0 -27
- data/spec/integration/server/timeout_spec.rb +0 -51
- data/spec/unit/body_spec.rb +0 -205
- data/spec/unit/builder_spec.rb +0 -372
- data/spec/unit/connection_spec.rb +0 -62
- data/spec/unit/events_spec.rb +0 -96
- data/spec/unit/parser_spec.rb +0 -209
- data/spec/unit/rack_handler_spec.rb +0 -60
@@ -0,0 +1,106 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe do
|
6
|
+
include_context :server_client_pair
|
7
|
+
|
8
|
+
let(:server_config) do
|
9
|
+
{ handlers: [SpecHandler, Hatetepe::Server::KeepAlive] }
|
10
|
+
end
|
11
|
+
let(:client_config) do
|
12
|
+
{ handlers: [Hatetepe::Client::KeepAlive] }
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:responses) do
|
16
|
+
requests.each do |request|
|
17
|
+
client.perform(request)
|
18
|
+
end
|
19
|
+
|
20
|
+
requests.map do |request|
|
21
|
+
request.served.sync.tap do |response|
|
22
|
+
response.finished.sync if response
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe Hatetepe::Server, 'with KeepAlive' do
|
28
|
+
let(:requests) do
|
29
|
+
[finished_request(:get, '/sleep'), finished_request]
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sends responses in the correct order' do
|
33
|
+
expect(responses[0].body).to eq('/sleep')
|
34
|
+
expect(responses[1].body).to eq('/')
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'and HTTP/1.1 request' do
|
38
|
+
let(:requests) { [finished_request] }
|
39
|
+
|
40
|
+
it 'assumes Connection: keep-alive' do
|
41
|
+
expect(client).not_to be_closed
|
42
|
+
expect(server).not_to be_closed
|
43
|
+
|
44
|
+
expect(responses[0].headers).to include('Connection' => 'keep-alive')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'and HTTP/1.0 request' do
|
49
|
+
let(:requests) do
|
50
|
+
[finished_request.tap { |r| r.http_version = 1.0 }]
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'assumes Connection: close' do
|
54
|
+
responses
|
55
|
+
tick
|
56
|
+
expect(client).to be_closed_by_remote
|
57
|
+
expect(server).to be_closed_by_self
|
58
|
+
|
59
|
+
expect(responses[0].headers).to include('Connection' => 'close')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'and Connection: keep-alive' do
|
64
|
+
let(:requests) do
|
65
|
+
[finished_request(:get, '/', { 'Connection' => 'keep-alive' })
|
66
|
+
.tap { |r| r.http_version = 1.0 }]
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'keeps the connection open' do
|
70
|
+
expect(client).not_to be_closed
|
71
|
+
expect(server).not_to be_closed
|
72
|
+
|
73
|
+
expect(responses[0].headers).to include('Connection' => 'keep-alive')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'and Connection: close' do
|
78
|
+
let(:requests) do
|
79
|
+
[finished_request(:get, '/', 'Connection' => 'close')]
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'closes the connection after sending the response' do
|
83
|
+
responses
|
84
|
+
tick
|
85
|
+
expect(client).to be_closed_by_remote
|
86
|
+
expect(server).to be_closed_by_self
|
87
|
+
|
88
|
+
expect(responses[0].headers).to include('Connection' => 'close')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe Hatetepe::Client, 'with KeepAlive' do
|
94
|
+
let(:requests) do
|
95
|
+
[finished_request(:get, '/close'), finished_request]
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'negotiates keep-alive before sending further requests' do
|
99
|
+
expect_any_instance_of(SpecHandler)
|
100
|
+
.to receive(:serve).once.and_call_original
|
101
|
+
|
102
|
+
expect(responses[0].body).to eq('/close')
|
103
|
+
expect(responses[1]).to be_nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'Smoke test' do
|
6
|
+
include_context :server_client_pair
|
7
|
+
|
8
|
+
let(:response) { client.request(:get, '/') }
|
9
|
+
let(:request) { intercepted_requests[0] }
|
10
|
+
|
11
|
+
it 'exchanges request and response' do
|
12
|
+
expect(response.status).to be(200)
|
13
|
+
expect(response.headers).to include('Content-Length' => '1')
|
14
|
+
expect(response.body).to eq('/')
|
15
|
+
|
16
|
+
expect(request.http_method).to be(:get)
|
17
|
+
expect(request.uri).to eq('/')
|
18
|
+
expect(request.headers).to include('Content-Length' => '0')
|
19
|
+
expect(request.body).to eq('')
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'Request body streaming' do
|
6
|
+
include_context :server_client_pair
|
7
|
+
|
8
|
+
let(:bodies) { [] }
|
9
|
+
let(:chunks) { [] }
|
10
|
+
|
11
|
+
before do
|
12
|
+
client.perform(request)
|
13
|
+
tick(2)
|
14
|
+
intercepted_requests[0].finished.on_progress do |chunk|
|
15
|
+
bodies << intercepted_requests[0].body.dup
|
16
|
+
chunks << chunk
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:request) { default_request }
|
21
|
+
|
22
|
+
subject! do
|
23
|
+
%w[foo bar baz].each do |chunk|
|
24
|
+
request.finished.progress(chunk)
|
25
|
+
tick
|
26
|
+
end
|
27
|
+
tick
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'progressively sends the body' do
|
31
|
+
expect(intercepted_requests[0].headers)
|
32
|
+
.to include('Transfer-Encoding' => 'chunked')
|
33
|
+
|
34
|
+
expect(bodies).to eq(%w[foo foobar foobarbaz])
|
35
|
+
expect(chunks).to eq(%w[foo bar baz])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'Response body streaming' do
|
40
|
+
include_context :server_client_pair
|
41
|
+
|
42
|
+
let(:bodies) { [] }
|
43
|
+
let(:chunks) { [] }
|
44
|
+
|
45
|
+
let(:response) { client.request(:get, '/streaming') }
|
46
|
+
|
47
|
+
subject! { response }
|
48
|
+
|
49
|
+
before do
|
50
|
+
response.finished.on_progress do |chunk|
|
51
|
+
bodies << response.body.dup
|
52
|
+
chunks << chunk
|
53
|
+
end
|
54
|
+
response.finished.sync
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'progressively receives the body' do
|
58
|
+
expect(bodies).to eq(%w[foo foobar foobarbaz])
|
59
|
+
expect(chunks).to eq(%w[foo bar baz])
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Hatetepe::Client, 'in connecting state' do
|
6
|
+
# connect to a non-existing host to provoke connection timeout
|
7
|
+
let(:address) { '1.2.3.4' }
|
8
|
+
let(:client_config) do
|
9
|
+
{ address: address, port: random_port,
|
10
|
+
handlers: [Hatetepe::Client::Timeouts] }
|
11
|
+
end
|
12
|
+
let!(:client) { Hatetepe::Client.new(client_config) }
|
13
|
+
|
14
|
+
after { client.close }
|
15
|
+
|
16
|
+
it 'times out after 2 seconds' do
|
17
|
+
# only test the config, and don't actually wait for 2 seconds
|
18
|
+
expect(client.config).to include(connect_timeout: 2.0)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'with :connect_timeout option' do
|
22
|
+
let(:client_config) do
|
23
|
+
{ address: address, port: random_port, connect_timeout: 0.01,
|
24
|
+
handlers: [Hatetepe::Client::Timeouts] }
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'times out after $n seconds' do
|
28
|
+
async_sleep(0.015)
|
29
|
+
if client.closed_reason == Errno::ENOTCONN
|
30
|
+
pending 'Requires network connection'
|
31
|
+
else
|
32
|
+
expect(client).to be_closed_by_timeout
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe Hatetepe::Client, 'in idle state' do
|
39
|
+
include_context :server_client_pair
|
40
|
+
|
41
|
+
let(:client_config) { { handlers: [Hatetepe::Client::Timeouts] } }
|
42
|
+
|
43
|
+
it 'times out after 2 seconds' do
|
44
|
+
# only test the config, and don't actually wait for 2 seconds
|
45
|
+
expect(client.config).to include(connect_timeout: 2.0)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'with :timeout option' do
|
49
|
+
let(:client_config) do
|
50
|
+
{ timeout: 0.01, handlers: [Hatetepe::Client::Timeouts] }
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'times out after $n seconds' do
|
54
|
+
async_sleep(0.015)
|
55
|
+
expect(client).to be_closed_by_timeout
|
56
|
+
expect(server).to be_closed_by_remote
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe Hatetepe::Server, 'in idle state' do
|
62
|
+
include_context :server_client_pair
|
63
|
+
|
64
|
+
let(:server_config) { { handlers: [Hatetepe::Server::Timeouts] } }
|
65
|
+
|
66
|
+
it 'times out after 2 seconds' do
|
67
|
+
# only test the config, and don't actually wait for 2 seconds
|
68
|
+
expect(server.config).to include(timeout: 2.0)
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'with :timeout option' do
|
72
|
+
let(:server_config) do
|
73
|
+
{ timeout: 0.01, handlers: [Hatetepe::Server::Timeouts] }
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'times out after $n seconds' do
|
77
|
+
async_sleep(0.015)
|
78
|
+
expect(client).to be_closed_by_remote
|
79
|
+
expect(server).to be_closed_by_timeout
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_context :server_client_pair do
|
4
|
+
let(:server_config) { { handlers: [SpecHandler] } }
|
5
|
+
let(:client_config) { { handlers: [] } }
|
6
|
+
let(:interceptor) { double('interceptor') }
|
7
|
+
|
8
|
+
let(:server_and_client) do
|
9
|
+
server_client_pair(server_config, client_config, interceptor)
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:intercepted_requests) { [] }
|
13
|
+
|
14
|
+
before do
|
15
|
+
allow(interceptor)
|
16
|
+
.to receive(:serve) { |request| intercepted_requests << request }
|
17
|
+
end
|
18
|
+
|
19
|
+
let!(:server) { server_and_client[0] }
|
20
|
+
let!(:client) { server_and_client[1] }
|
21
|
+
|
22
|
+
after do
|
23
|
+
server.close
|
24
|
+
client.close
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,16 +1,47 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
if ENV['COVERAGE'] == 'true'
|
4
|
+
require 'simplecov'
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
9
|
+
Coveralls::SimpleCov::Formatter
|
10
|
+
]
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
command_name 'spec:unit'
|
14
|
+
|
15
|
+
add_filter 'config'
|
16
|
+
add_filter 'spec'
|
17
|
+
end
|
4
18
|
end
|
5
19
|
|
6
|
-
require
|
20
|
+
require 'hatetepe'
|
21
|
+
require 'weakref'
|
22
|
+
|
23
|
+
require 'awesome_print'
|
24
|
+
|
25
|
+
require 'devtools/spec_helper'
|
26
|
+
|
27
|
+
RSpec.configure do |config|
|
28
|
+
config.include(SpecHelper)
|
29
|
+
|
30
|
+
config.around do |example|
|
31
|
+
EM.run do
|
32
|
+
fiber = Fiber.new do
|
33
|
+
example.call
|
34
|
+
EM.stop
|
35
|
+
EM.next_tick {}
|
36
|
+
end
|
37
|
+
|
38
|
+
EM.add_timer(0.5) do
|
39
|
+
EM.stop
|
40
|
+
EM.next_tick {}
|
41
|
+
raise 'Example timed out'
|
42
|
+
end
|
7
43
|
|
8
|
-
|
9
|
-
c.around do |example|
|
10
|
-
EM.synchrony do
|
11
|
-
EM.heartbeat_interval = 0.01
|
12
|
-
example.call
|
13
|
-
EM.next_tick { EM.stop }
|
44
|
+
fiber.resume
|
14
45
|
end
|
15
46
|
end
|
16
47
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class SpecHandler
|
4
|
+
def initialize(config, server, connection)
|
5
|
+
end
|
6
|
+
|
7
|
+
def serve(request)
|
8
|
+
response = response_for(request)
|
9
|
+
request.served.fulfill(response)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def response_for(request)
|
15
|
+
case request.uri
|
16
|
+
when '/' then root_response.tap(&fulfill)
|
17
|
+
when '/sleep' then sleep_response.tap(&fulfill)
|
18
|
+
when '/close' then close_response.tap(&fulfill)
|
19
|
+
when '/streaming' then streaming_response
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def fulfill
|
24
|
+
proc { |response| response.finished.fulfill }
|
25
|
+
end
|
26
|
+
|
27
|
+
def root_response
|
28
|
+
Hatetepe::Response.new(200, { 'Content-Type' => 'text/plain' }, '/')
|
29
|
+
end
|
30
|
+
|
31
|
+
def sleep_response
|
32
|
+
SpecHelper.async_sleep(0.01)
|
33
|
+
Hatetepe::Response.new(200, { 'Content-Type' => 'text/plain' }, '/sleep')
|
34
|
+
end
|
35
|
+
|
36
|
+
def close_response
|
37
|
+
Hatetepe::Response.new(200,
|
38
|
+
{ 'Content-Type' => 'text/plain',
|
39
|
+
'Connection' => 'close' },
|
40
|
+
'/close')
|
41
|
+
end
|
42
|
+
|
43
|
+
def streaming_response
|
44
|
+
response = Hatetepe::Response.new(200, 'Content-Type' => 'text/plain')
|
45
|
+
SpecHelper.defer do
|
46
|
+
%w[foo bar baz].each do |chunk|
|
47
|
+
SpecHelper.tick(2)
|
48
|
+
response.body << chunk
|
49
|
+
response.finished.progress(chunk)
|
50
|
+
end
|
51
|
+
response.finished.fulfill
|
52
|
+
end
|
53
|
+
response
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module SpecHelper
|
4
|
+
def async_sleep(seconds)
|
5
|
+
fiber = Fiber.current
|
6
|
+
EM.add_timer(seconds) { fiber.resume }
|
7
|
+
Fiber.yield
|
8
|
+
end
|
9
|
+
module_function :async_sleep
|
10
|
+
|
11
|
+
def tick(ticks = 1)
|
12
|
+
fiber = Fiber.current
|
13
|
+
EM.next_tick { fiber.resume }
|
14
|
+
Fiber.yield
|
15
|
+
|
16
|
+
tick(ticks - 1) if ticks > 1
|
17
|
+
end
|
18
|
+
module_function :tick
|
19
|
+
|
20
|
+
def defer
|
21
|
+
EM.next_tick do
|
22
|
+
Fiber.new { yield }.resume
|
23
|
+
end
|
24
|
+
end
|
25
|
+
module_function :defer
|
26
|
+
|
27
|
+
def default_request(http_method = :get, uri = '/', headers = {})
|
28
|
+
Hatetepe::Request.new(http_method, uri, headers)
|
29
|
+
end
|
30
|
+
|
31
|
+
def finished_request(*args)
|
32
|
+
default_request(*args).tap { |request| request.finished.fulfill }
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_response(status = 200, headers = {})
|
36
|
+
Hatetepe::Response.new(status, headers)
|
37
|
+
end
|
38
|
+
|
39
|
+
def finished_response(*args)
|
40
|
+
default_response(*args).tap { |response| response.finished.fulfill }
|
41
|
+
end
|
42
|
+
|
43
|
+
def localhost
|
44
|
+
'127.0.0.1'
|
45
|
+
end
|
46
|
+
|
47
|
+
def random_port
|
48
|
+
server = TCPServer.new(localhost, 0)
|
49
|
+
server.addr[1]
|
50
|
+
ensure
|
51
|
+
server.shutdown
|
52
|
+
end
|
53
|
+
|
54
|
+
def server_client_pair(server_config, client_config, interceptor)
|
55
|
+
server = nil
|
56
|
+
inspect_server(server_config) do |_, s, _|
|
57
|
+
server = s
|
58
|
+
interceptor
|
59
|
+
end
|
60
|
+
|
61
|
+
config = { address: localhost, port: random_port }
|
62
|
+
Hatetepe::Server.start(config.merge(server_config))
|
63
|
+
client = Hatetepe::Client.new(config.merge(client_config))
|
64
|
+
|
65
|
+
tick
|
66
|
+
[server, client]
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect_server(server_config, &block)
|
70
|
+
interceptor_class = double('interceptor_class')
|
71
|
+
allow(interceptor_class).to receive(:new, &block)
|
72
|
+
server_config[:handlers].unshift(interceptor_class)
|
73
|
+
end
|
74
|
+
end
|