hatetepe 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/README.md +21 -16
  2. data/hatetepe.gemspec +1 -2
  3. data/lib/hatetepe/body.rb +5 -3
  4. data/lib/hatetepe/builder.rb +6 -3
  5. data/lib/hatetepe/cli.rb +27 -5
  6. data/lib/hatetepe/client.rb +157 -82
  7. data/lib/hatetepe/client/keep_alive.rb +58 -0
  8. data/lib/hatetepe/client/pipeline.rb +19 -0
  9. data/lib/hatetepe/connection.rb +42 -0
  10. data/lib/hatetepe/deferred_status_fix.rb +11 -0
  11. data/lib/hatetepe/message.rb +4 -4
  12. data/lib/hatetepe/parser.rb +3 -4
  13. data/lib/hatetepe/request.rb +19 -6
  14. data/lib/hatetepe/response.rb +11 -3
  15. data/lib/hatetepe/server.rb +115 -85
  16. data/lib/hatetepe/{app.rb → server/app.rb} +7 -2
  17. data/lib/hatetepe/server/keep_alive.rb +61 -0
  18. data/lib/hatetepe/server/pipeline.rb +24 -0
  19. data/lib/hatetepe/{proxy.rb → server/proxy.rb} +5 -11
  20. data/lib/hatetepe/version.rb +1 -1
  21. data/lib/rack/handler/hatetepe.rb +1 -4
  22. data/spec/integration/cli/start_spec.rb +75 -123
  23. data/spec/integration/client/keep_alive_spec.rb +74 -0
  24. data/spec/integration/server/keep_alive_spec.rb +99 -0
  25. data/spec/spec_helper.rb +41 -16
  26. data/spec/unit/app_spec.rb +16 -5
  27. data/spec/unit/builder_spec.rb +4 -4
  28. data/spec/unit/client/pipeline_spec.rb +40 -0
  29. data/spec/unit/client_spec.rb +355 -199
  30. data/spec/unit/connection_spec.rb +64 -0
  31. data/spec/unit/parser_spec.rb +3 -2
  32. data/spec/unit/proxy_spec.rb +9 -18
  33. data/spec/unit/rack_handler_spec.rb +2 -12
  34. data/spec/unit/server_spec.rb +154 -60
  35. metadata +31 -36
  36. data/.rspec +0 -1
  37. data/.travis.yml +0 -3
  38. data/.yardopts +0 -1
  39. data/lib/hatetepe/pipeline.rb +0 -27
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+ require "hatetepe/connection"
3
+
4
+ describe Hatetepe::Connection do
5
+ let(:conn) { Hatetepe::Connection.allocate }
6
+
7
+ let(:peername) { "\x02\x00\x86\xF6\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" }
8
+ let(:address) { "127.0.0.1" }
9
+ let(:port) { 34550 }
10
+
11
+ before { conn.stub :get_peername => peername }
12
+
13
+ it "inherits from EM::Connection" do
14
+ conn.should be_an(EM::Connection)
15
+ end
16
+
17
+ describe "#remote_address" do
18
+ it "returns the remote peer's address" do
19
+ conn.remote_address.should == address
20
+ end
21
+ end
22
+
23
+ describe "#remote_port" do
24
+ it "returns the remote peer's port" do
25
+ conn.remote_port.should == port
26
+ end
27
+ end
28
+
29
+ describe "#close_connection" do
30
+ before { EM::Connection.any_instance.stub :close_connection }
31
+
32
+ it "sets the closed-by-self flag" do
33
+ pending "How to test a call to super?"
34
+
35
+ conn.close_connection
36
+ conn.should be_closed
37
+ conn.should be_closed_by_self
38
+ end
39
+
40
+ let(:arg) { stub "arg" }
41
+
42
+ it "calls EM::Connection.close_connection" do
43
+ pending "How to test a call to super?"
44
+
45
+ EM::Connection.any_instance.should_receive(:close_connection).with arg
46
+ conn.close_connection arg
47
+ end
48
+ end
49
+
50
+ describe "#unbind" do
51
+ it "sets the closed-by-remote flag" do
52
+ conn.unbind
53
+ conn.should be_closed
54
+ conn.should be_closed_by_remote
55
+ end
56
+
57
+ it "doesn't overwrite an existing closed-by flag" do
58
+ conn.stub :closed? => true
59
+ conn.unbind
60
+ conn.should be_closed
61
+ conn.should_not be_closed_by_remote
62
+ end
63
+ end
64
+ end
@@ -90,14 +90,15 @@ describe Hatetepe::Parser do
90
90
  }
91
91
 
92
92
  context "#on_request {|request| ... }" do
93
- it "evals the block when a request line comes in" do
93
+ it "evals the block when the request headers have arrived" do
94
94
  block.should_receive(:call) {|request|
95
95
  request.should equal(parser.message)
96
96
 
97
97
  request.verb.should == "POST"
98
98
  request.uri.should == "/"
99
99
  request.http_version.should == "1.1"
100
- request.headers.should be_empty
100
+ request.headers.should == {"Transfer-Encoding" => "chunked",
101
+ "Bar" => "baz"}
101
102
  }
102
103
 
103
104
  parser.on_request &block
@@ -1,16 +1,16 @@
1
1
  require "spec_helper"
2
- require "hatetepe/proxy"
2
+ require "hatetepe/server"
3
3
 
4
- describe Hatetepe::Proxy do
4
+ describe Hatetepe::Server::Proxy do
5
5
  let(:app) { stub "app" }
6
6
 
7
7
  describe "#initialize(app)" do
8
8
  it "sets the app" do
9
- Hatetepe::Proxy.new(app).app.should equal(app)
9
+ Hatetepe::Server::Proxy.new(app).app.should equal(app)
10
10
  end
11
11
  end
12
12
 
13
- let(:proxy) { Hatetepe::Proxy.new app }
13
+ let(:proxy) { Hatetepe::Server::Proxy.new app }
14
14
  let(:target) { stub "target" }
15
15
  let(:env) { {} }
16
16
  let(:client) { stub "client", :<< => nil }
@@ -133,22 +133,13 @@ describe Hatetepe::Proxy do
133
133
  proxy.build_request(env, target).uri.should == "/bar/foo"
134
134
  end
135
135
 
136
- it "sets X-Forwarded-For header" do
137
- xff = proxy.build_request(env, target).headers["X-Forwarded-For"]
138
- env["REMOTE_ADDR"].should == xff
139
- end
140
-
141
- it "adds the target to Host header" do
142
- host = "localhost:3000"
143
- proxy.build_request(env, target).headers["Host"].should == host
144
-
145
- base_request.headers["Host"] = host
146
- host = "localhost:3000, localhost:3000"
147
- proxy.build_request(env, target).headers["Host"].should == host
148
- end
149
-
150
136
  it "builds a new request" do
151
137
  proxy.build_request(env, target).should_not equal(base_request)
152
138
  end
139
+
140
+ it "sets version to HTTP/1.1" do
141
+ base_request.http_version = "1.0"
142
+ proxy.build_request(env, target).http_version.should == "1.1"
143
+ end
153
144
  end
154
145
  end
@@ -31,17 +31,6 @@ describe Rack::Handler::Hatetepe do
31
31
  Rack::Handler::Hatetepe.run app, options
32
32
  end
33
33
 
34
- it "yields the server" do
35
- Hatetepe::Server.stub :start => server
36
-
37
- srvr = nil
38
- Rack::Handler::Hatetepe.run(app) {|s|
39
- srvr = s
40
- EM.stop
41
- }
42
- srvr.should equal(server)
43
- end
44
-
45
34
  it "can be stopped by sending SIGTERM or SIGINT" do
46
35
  EM.should_receive(:synchrony) {|&block| block.call }
47
36
 
@@ -63,8 +52,9 @@ describe Rack::Handler::Hatetepe do
63
52
  Hatetepe::Server.should_receive(:start) {|opts|
64
53
  opts[:host].should == "0.0.0.0"
65
54
  opts[:port].should == 8080
55
+ EM.stop
66
56
  }
67
- Rack::Handler::Hatetepe.run(app) { EM.stop }
57
+ Rack::Handler::Hatetepe.run app
68
58
  end
69
59
  end
70
60
  end
@@ -5,16 +5,13 @@ describe Hatetepe::Server do
5
5
  let(:server) {
6
6
  Hatetepe::Server.allocate.tap {|s|
7
7
  s.send :initialize, config
8
+ s.stub :comm_inactivity_timeout=
8
9
  s.post_init
9
10
  s.requests << request
10
11
  }
11
12
  }
12
- let(:request) { stub "request", :to_hash => env }
13
- let(:env) {
14
- {
15
- "rack.input" => Hatetepe::Body.new
16
- }
17
- }
13
+ let(:request) { Hatetepe::Request.new :get, "/" }
14
+ let(:env) { request.to_h }
18
15
 
19
16
  let(:app) { stub "app" }
20
17
  let(:host) { "127.0.4.1" }
@@ -25,10 +22,24 @@ describe Hatetepe::Server do
25
22
  :app => app,
26
23
  :host => host,
27
24
  :port => port,
28
- :errors => errors
25
+ :errors => errors,
26
+ :timeout => 0.0123
29
27
  }
30
28
  }
31
29
 
30
+ before do
31
+ server.stub :sockaddr => [42424, "127.0.42.1"]
32
+ @old_env, ENV["RACK_ENV"] = ENV["RACK_ENV"], "testing"
33
+ end
34
+
35
+ after do
36
+ ENV["RACK_ENV"] = @old_env
37
+ end
38
+
39
+ it "inherits from Hatetepe::Connection" do
40
+ server.should be_a(Hatetepe::Connection)
41
+ end
42
+
32
43
  context ".start(config)" do
33
44
  it "starts an EventMachine server" do
34
45
  args = [host, port, Hatetepe::Server, config]
@@ -40,23 +51,11 @@ describe Hatetepe::Server do
40
51
 
41
52
  context "#initialize(config)" do
42
53
  let(:server) { Hatetepe::Server.allocate }
43
- let(:builder) { stub "app builder", :to_app => to_app }
44
- let(:to_app) { stub "to_app" }
45
-
46
- it "builds the app" do
47
- Rack::Builder.stub :new => builder
48
- builder.should_receive(:use).with Hatetepe::Pipeline
49
- builder.should_receive(:use).with Hatetepe::App
50
- builder.should_receive(:use).with Hatetepe::Proxy
51
- builder.should_receive(:run).with app
52
-
53
- server.send :initialize, config
54
- server.app.should equal(to_app)
55
- end
56
54
 
57
55
  it "sets up the error stream" do
58
56
  server.send :initialize, config
59
57
  server.errors.should equal(errors)
58
+ server.config[:errors].should be_nil
60
59
  end
61
60
 
62
61
  it "uses stderr as default error stream" do
@@ -64,30 +63,63 @@ describe Hatetepe::Server do
64
63
  server.send :initialize, config
65
64
  server.errors.should equal($stderr)
66
65
  end
66
+
67
+ it "assumes a default connection inactivity timeout of 1 seconds" do
68
+ server.send :initialize, {}
69
+ server.config[:timeout].should equal(1)
70
+ end
67
71
  end
68
72
 
69
73
  context "#post_init" do
70
- let(:server) {
71
- Hatetepe::Server.allocate.tap {|s| s.post_init }
72
- }
74
+ let :server do
75
+ Hatetepe::Server.allocate.tap do |s|
76
+ s.send :initialize, config
77
+ s.stub :comm_inactivity_timeout=
78
+ end
79
+ end
73
80
 
74
81
  it "sets up the request queue" do
82
+ server.post_init
75
83
  server.requests.should be_an(Array)
76
84
  server.requests.should be_empty
77
85
  end
78
86
 
79
87
  it "sets up the parser" do
88
+ server.post_init
80
89
  server.parser.should respond_to(:<<)
81
90
  server.parser.on_request[0].should == server.requests.method(:<<)
82
91
  end
83
92
 
84
93
  it "sets up the builder" do
94
+ server.post_init
85
95
  server.builder.on_write[0].should == server.method(:send_data)
86
96
  end
97
+
98
+ it "builds the app" do
99
+ server.post_init
100
+ server.app.should be_a(Hatetepe::Server::Pipeline)
101
+ server.app.app.should be_a(Hatetepe::Server::App)
102
+ server.app.app.app.should be_a(Hatetepe::Server::KeepAlive)
103
+ server.app.app.app.app.should be_a(Hatetepe::Server::Proxy)
104
+ server.app.app.app.app.app.should equal(app)
105
+ end
106
+
107
+ it "starts the connection inactivity tracking" do
108
+ server.should_receive(:comm_inactivity_timeout=).with 0.0123
109
+ server.post_init
110
+ end
111
+
112
+ it "enables request processing" do
113
+ server.post_init
114
+ server.processing_enabled.should be_true
115
+ end
87
116
  end
88
117
 
89
118
  context "#receive_data(data)" do
90
- before { server.stub :close_connection_after_writing }
119
+ before do
120
+ ENV.delete "RACK_ENV"
121
+ server.stub :close_connection
122
+ end
91
123
 
92
124
  it "feeds data into the parser" do
93
125
  data = stub("data")
@@ -102,13 +134,29 @@ describe Hatetepe::Server do
102
134
  server.receive_data "irrelevant data"
103
135
  end
104
136
 
137
+ it "re-raises parsing errors if RACK_ENV is testing" do
138
+ ENV["RACK_ENV"] = "testing"
139
+ server.parser.should_receive(:<<).and_raise Hatetepe::ParserError
140
+
141
+ expect {
142
+ server.receive_data "irrelevant data"
143
+ }.to raise_error(Hatetepe::ParserError)
144
+ end
145
+
105
146
  it "closes the connection when catching an exception" do
106
- server.parser.should_receive(:<<).and_raise
147
+ server.parser.should_receive(:<<).and_raise Exception
107
148
  server.should_receive :close_connection_after_writing
108
149
 
109
150
  server.receive_data ""
110
151
  end
111
152
 
153
+ it "re-raises caught exceptions" do
154
+ ENV["RACK_ENV"] = "testing"
155
+ server.parser.should_receive(:<<).and_raise Exception
156
+
157
+ expect { server.receive_data "" }.to raise_error(Exception)
158
+ end
159
+
112
160
  it "logs caught exceptions" do
113
161
  server.parser.should_receive(:<<).and_raise "error message"
114
162
  errors.should_receive(:<<) {|str|
@@ -121,6 +169,11 @@ describe Hatetepe::Server do
121
169
  end
122
170
 
123
171
  context "#process" do
172
+ before do
173
+ request.stub :to_h => env
174
+ app.stub :call => [-1]
175
+ end
176
+
124
177
  it "puts useful stuff into env[]" do
125
178
  app.should_receive(:call) {|e|
126
179
  e.should equal(env)
@@ -135,6 +188,9 @@ describe Hatetepe::Server do
135
188
  e["SERVER_NAME"].should == host
136
189
  e["SERVER_NAME"].should_not equal(host)
137
190
  e["SERVER_PORT"].should == String(port)
191
+ e["REMOTE_ADDR"].should == server.remote_address
192
+ e["REMOTE_ADDR"].should_not equal(server.remote_address)
193
+ e["REMOTE_PORT"].should == String(server.remote_port)
138
194
  e["HTTP_HOST"].should == "#{host}:#{port}"
139
195
 
140
196
  [-1]
@@ -150,6 +206,32 @@ describe Hatetepe::Server do
150
206
  }
151
207
  server.process
152
208
  end
209
+
210
+ it "is a no-op if processing is disabled" do
211
+ server.processing_enabled = false
212
+ app.should_not_receive :call
213
+ server.process
214
+ end
215
+
216
+ let(:another_request) { Hatetepe::Request.new :get, "/asdf" }
217
+
218
+ it "disables the connection timeout until the last request is finished" do
219
+ server.requests << another_request
220
+
221
+ server.should_receive(:comm_inactivity_timeout=).with 0
222
+ server.process
223
+
224
+ server.should_not_receive(:comm_inactivity_timeout=).with config[:timeout]
225
+ server.requests.delete request
226
+ request.succeed
227
+
228
+ server.rspec_verify
229
+ server.rspec_reset
230
+
231
+ server.should_receive(:comm_inactivity_timeout=).with config[:timeout]
232
+ server.requests.delete another_request
233
+ another_request.succeed
234
+ end
153
235
  end
154
236
 
155
237
  context "env[stream.start].call(response)" do
@@ -176,22 +258,11 @@ describe Hatetepe::Server do
176
258
  server.process
177
259
  end
178
260
 
261
+ # TODO this should be moved to a Server::Pipeline spec
179
262
  it "waits for the previous request's response to finish" do
180
263
  server.builder.should_not_receive :response
181
264
  server.process
182
265
  end
183
-
184
- it "initiates the response" do
185
- server.builder.should_receive(:response_line) {|code|
186
- code.should equal(response[0])
187
- }
188
- server.builder.should_receive(:headers) {|headers|
189
- headers["Key"].should equal(response[1]["Key"])
190
- headers["Server"].should == "hatetepe/#{Hatetepe::VERSION}"
191
- }
192
- previous.succeed
193
- server.process
194
- end
195
266
  end
196
267
 
197
268
  context "env[stream.send].call(chunk)" do
@@ -206,55 +277,78 @@ describe Hatetepe::Server do
206
277
 
207
278
  context "env[stream.close].call" do
208
279
  before {
209
- server.stub :close_connection
210
280
  server.builder.stub :complete
211
281
  request.stub :succeed
212
282
  }
213
283
 
214
- it "completes the response" do
215
- server.builder.should_receive :complete
284
+ it "leaves the connection open" do
285
+ server.should_not_receive :close_connection
216
286
  app.stub(:call) {|e|
287
+ server.requests << stub("another request")
217
288
  e["stream.close"].call
218
289
  [-1]
219
290
  }
220
291
  server.process
221
292
  end
222
293
 
223
- it "succeeds the request" do
224
- request.should_receive :succeed
294
+ it "deletes itself and stream.send from env[] to prevent multiple calls" do
225
295
  app.stub(:call) {|e|
226
296
  e["stream.close"].call
297
+ e.key?("stream.send").should be_false
298
+ e.key?("stream.close").should be_false
227
299
  [-1]
228
300
  }
229
301
  server.process
230
302
  end
303
+ end
304
+
305
+ context "#start_response(response)" do
306
+ let(:previous) { EM::DefaultDeferrable.new }
307
+ let(:response) { [200, {"Key" => "value"}, Rack::STREAMING] }
231
308
 
232
- it "leaves the connection open" do
233
- server.should_not_receive :close_connection
234
- app.stub(:call) {|e|
235
- server.requests << stub("another request")
236
- e["stream.close"].call
237
- [-1]
309
+ before {
310
+ server.requests.unshift previous
311
+ app.stub(:call) {|e| response }
312
+ request.stub :succeed
313
+ server.builder.stub :response_line
314
+ server.builder.stub :headers
315
+ }
316
+
317
+ it "initiates the response" do
318
+ server.builder.should_receive(:response_line) {|code|
319
+ code.should equal(response[0])
238
320
  }
321
+ server.builder.should_receive(:headers) {|headers|
322
+ headers["Key"].should equal(response[1]["Key"])
323
+ headers["Server"].should == "hatetepe/#{Hatetepe::VERSION}"
324
+ }
325
+ previous.succeed
239
326
  server.process
240
327
  end
241
-
242
- it "closes the connection if there are no more requests" do
243
- server.should_receive(:close_connection).with true
244
- app.stub(:call) {|e|
328
+ end
329
+
330
+ context "#close_response(request)" do
331
+ before do
332
+ server.builder.stub :complete
333
+ request.stub :succeed
334
+ app.stub :call do |e|
245
335
  e["stream.close"].call
246
336
  [-1]
247
- }
337
+ end
338
+ end
339
+
340
+ it "removes the request from the request queue" do
248
341
  server.process
342
+ server.requests.should be_empty
249
343
  end
250
344
 
251
- it "deletes itself and stream.send from env[] to prevent multiple calls" do
252
- app.stub(:call) {|e|
253
- e["stream.close"].call
254
- e.key?("stream.send").should be_false
255
- e.key?("stream.close").should be_false
256
- [-1]
257
- }
345
+ it "completes the response" do
346
+ server.builder.should_receive :complete
347
+ server.process
348
+ end
349
+
350
+ it "succeeds the request" do
351
+ request.should_receive :succeed
258
352
  server.process
259
353
  end
260
354
  end