polyphony 0.19 → 0.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop.yml +87 -1
  4. data/CHANGELOG.md +35 -0
  5. data/Gemfile.lock +17 -6
  6. data/README.md +200 -139
  7. data/Rakefile +4 -4
  8. data/TODO.md +35 -7
  9. data/bin/poly +11 -0
  10. data/docs/getting-started/getting-started.md +1 -1
  11. data/docs/summary.md +3 -0
  12. data/docs/technical-overview/exception-handling.md +94 -0
  13. data/docs/technical-overview/fiber-scheduling.md +99 -0
  14. data/examples/core/cancel.rb +8 -4
  15. data/examples/core/channel_echo.rb +18 -17
  16. data/examples/core/defer.rb +12 -0
  17. data/examples/core/enumerator.rb +4 -4
  18. data/examples/core/fiber_error.rb +9 -0
  19. data/examples/core/fiber_error_with_backtrace.rb +73 -0
  20. data/examples/core/fork.rb +6 -6
  21. data/examples/core/genserver.rb +16 -8
  22. data/examples/core/lock.rb +3 -3
  23. data/examples/core/move_on.rb +4 -3
  24. data/examples/core/move_on_twice.rb +5 -5
  25. data/examples/core/move_on_with_ensure.rb +8 -11
  26. data/examples/core/move_on_with_value.rb +14 -0
  27. data/examples/core/{multiple_spawn.rb → multiple_spin.rb} +5 -5
  28. data/examples/core/nested_cancel.rb +5 -5
  29. data/examples/core/{nested_multiple_spawn.rb → nested_multiple_spin.rb} +6 -6
  30. data/examples/core/nested_spin.rb +17 -0
  31. data/examples/core/pingpong.rb +21 -0
  32. data/examples/core/pulse.rb +4 -5
  33. data/examples/core/resource.rb +6 -4
  34. data/examples/core/resource_cancel.rb +6 -9
  35. data/examples/core/resource_delegate.rb +3 -3
  36. data/examples/core/sleep.rb +3 -3
  37. data/examples/core/sleep_spin.rb +19 -0
  38. data/examples/core/snooze.rb +32 -0
  39. data/examples/core/spin.rb +14 -0
  40. data/examples/core/{spawn_cancel.rb → spin_cancel.rb} +6 -7
  41. data/examples/core/spin_error.rb +17 -0
  42. data/examples/core/spin_error_backtrace.rb +30 -0
  43. data/examples/core/spin_uncaught_error.rb +15 -0
  44. data/examples/core/supervisor.rb +8 -8
  45. data/examples/core/supervisor_with_cancel_scope.rb +7 -7
  46. data/examples/core/supervisor_with_error.rb +8 -8
  47. data/examples/core/supervisor_with_manual_move_on.rb +6 -7
  48. data/examples/core/suspend.rb +13 -0
  49. data/examples/core/thread.rb +1 -1
  50. data/examples/core/thread_cancel.rb +9 -11
  51. data/examples/core/thread_pool.rb +18 -14
  52. data/examples/core/throttle.rb +7 -7
  53. data/examples/core/timeout.rb +3 -3
  54. data/examples/fs/read.rb +7 -9
  55. data/examples/http/config.ru +7 -3
  56. data/examples/http/cuba.ru +22 -0
  57. data/examples/http/happy_eyeballs.rb +6 -4
  58. data/examples/http/http_client.rb +1 -1
  59. data/examples/http/http_get.rb +1 -1
  60. data/examples/http/http_parse_experiment.rb +21 -16
  61. data/examples/http/http_proxy.rb +28 -26
  62. data/examples/http/http_server.rb +10 -10
  63. data/examples/http/http_server_forked.rb +6 -5
  64. data/examples/http/http_server_throttled.rb +3 -3
  65. data/examples/http/http_ws_server.rb +11 -11
  66. data/examples/http/https_raw_client.rb +1 -1
  67. data/examples/http/https_server.rb +8 -8
  68. data/examples/http/https_wss_server.rb +13 -11
  69. data/examples/http/rack_server.rb +2 -2
  70. data/examples/http/rack_server_https.rb +4 -4
  71. data/examples/http/rack_server_https_forked.rb +5 -5
  72. data/examples/http/websocket_secure_server.rb +6 -6
  73. data/examples/http/websocket_server.rb +5 -5
  74. data/examples/interfaces/pg_client.rb +4 -4
  75. data/examples/interfaces/pg_pool.rb +13 -6
  76. data/examples/interfaces/pg_transaction.rb +5 -4
  77. data/examples/interfaces/redis_channels.rb +15 -11
  78. data/examples/interfaces/redis_client.rb +2 -2
  79. data/examples/interfaces/redis_pubsub.rb +2 -1
  80. data/examples/interfaces/redis_pubsub_perf.rb +13 -9
  81. data/examples/io/backticks.rb +11 -0
  82. data/examples/io/cat.rb +4 -5
  83. data/examples/io/echo_client.rb +9 -4
  84. data/examples/io/echo_client_from_stdin.rb +20 -0
  85. data/examples/io/echo_pipe.rb +7 -8
  86. data/examples/io/echo_server.rb +8 -6
  87. data/examples/io/echo_server_with_timeout.rb +13 -10
  88. data/examples/io/echo_stdin.rb +3 -3
  89. data/examples/io/httparty.rb +2 -2
  90. data/examples/io/httparty_multi.rb +8 -4
  91. data/examples/io/httparty_threaded.rb +6 -2
  92. data/examples/io/io_read.rb +2 -2
  93. data/examples/io/irb.rb +16 -4
  94. data/examples/io/net-http.rb +3 -3
  95. data/examples/io/open.rb +17 -0
  96. data/examples/io/system.rb +3 -3
  97. data/examples/io/tcpserver.rb +15 -0
  98. data/examples/io/tcpsocket.rb +6 -5
  99. data/examples/performance/multi_snooze.rb +29 -0
  100. data/examples/performance/{perf_snooze.rb → snooze.rb} +7 -5
  101. data/examples/performance/snooze_raw.rb +39 -0
  102. data/ext/gyro/async.c +165 -0
  103. data/ext/gyro/child.c +167 -0
  104. data/ext/{ev → gyro}/extconf.rb +4 -3
  105. data/ext/gyro/gyro.c +316 -0
  106. data/ext/{ev/ev.h → gyro/gyro.h} +12 -7
  107. data/ext/gyro/gyro_ext.c +23 -0
  108. data/ext/{ev → gyro}/io.c +65 -57
  109. data/ext/{ev → gyro}/libev.h +0 -0
  110. data/ext/gyro/signal.c +117 -0
  111. data/ext/{ev → gyro}/socket.c +61 -6
  112. data/ext/gyro/timer.c +199 -0
  113. data/ext/libev/Changes +35 -0
  114. data/ext/libev/README +2 -1
  115. data/ext/libev/ev.c +213 -151
  116. data/ext/libev/ev.h +95 -88
  117. data/ext/libev/ev_epoll.c +26 -15
  118. data/ext/libev/ev_kqueue.c +11 -5
  119. data/ext/libev/ev_linuxaio.c +642 -0
  120. data/ext/libev/ev_poll.c +13 -8
  121. data/ext/libev/ev_port.c +5 -2
  122. data/ext/libev/ev_vars.h +14 -3
  123. data/ext/libev/ev_wrap.h +16 -0
  124. data/lib/ev_ext.bundle +0 -0
  125. data/lib/polyphony.rb +46 -50
  126. data/lib/polyphony/auto_run.rb +12 -0
  127. data/lib/polyphony/core/cancel_scope.rb +11 -7
  128. data/lib/polyphony/core/channel.rb +16 -9
  129. data/lib/polyphony/core/coprocess.rb +101 -51
  130. data/lib/polyphony/core/exceptions.rb +14 -12
  131. data/lib/polyphony/core/resource_pool.rb +21 -8
  132. data/lib/polyphony/core/supervisor.rb +10 -5
  133. data/lib/polyphony/core/sync.rb +7 -6
  134. data/lib/polyphony/core/thread.rb +4 -4
  135. data/lib/polyphony/core/thread_pool.rb +4 -4
  136. data/lib/polyphony/core/throttler.rb +6 -4
  137. data/lib/polyphony/extensions/core.rb +253 -0
  138. data/lib/polyphony/extensions/io.rb +28 -16
  139. data/lib/polyphony/extensions/openssl.rb +2 -1
  140. data/lib/polyphony/extensions/socket.rb +47 -52
  141. data/lib/polyphony/http.rb +4 -3
  142. data/lib/polyphony/http/agent.rb +68 -57
  143. data/lib/polyphony/http/server.rb +5 -5
  144. data/lib/polyphony/http/server/http1.rb +268 -0
  145. data/lib/polyphony/http/server/http2.rb +62 -0
  146. data/lib/polyphony/http/server/http2_stream.rb +104 -0
  147. data/lib/polyphony/http/server/rack.rb +64 -0
  148. data/lib/polyphony/http/server/request.rb +119 -0
  149. data/lib/polyphony/net.rb +26 -15
  150. data/lib/polyphony/postgres.rb +17 -13
  151. data/lib/polyphony/redis.rb +16 -15
  152. data/lib/polyphony/version.rb +1 -1
  153. data/lib/polyphony/websocket.rb +11 -4
  154. data/polyphony.gemspec +13 -9
  155. data/test/eg.rb +27 -0
  156. data/test/helper.rb +25 -0
  157. data/test/run.rb +5 -0
  158. data/test/test_async.rb +33 -0
  159. data/test/test_coprocess.rb +239 -77
  160. data/test/test_core.rb +95 -61
  161. data/test/test_gyro.rb +148 -0
  162. data/test/test_http_server.rb +313 -0
  163. data/test/test_io.rb +79 -27
  164. data/test/test_kernel.rb +22 -12
  165. data/test/test_signal.rb +36 -0
  166. data/test/test_timer.rb +24 -0
  167. metadata +89 -33
  168. data/examples/core/nested_async.rb +0 -17
  169. data/examples/core/next_tick.rb +0 -12
  170. data/examples/core/sleep_spawn.rb +0 -19
  171. data/examples/core/spawn.rb +0 -14
  172. data/examples/core/spawn_error.rb +0 -28
  173. data/examples/performance/perf_multi_snooze.rb +0 -21
  174. data/ext/ev/async.c +0 -168
  175. data/ext/ev/child.c +0 -169
  176. data/ext/ev/ev_ext.c +0 -23
  177. data/ext/ev/ev_module.c +0 -242
  178. data/ext/ev/signal.c +0 -119
  179. data/ext/ev/timer.c +0 -197
  180. data/lib/polyphony/core/fiber_pool.rb +0 -98
  181. data/lib/polyphony/extensions/kernel.rb +0 -169
  182. data/lib/polyphony/http/http1_adapter.rb +0 -254
  183. data/lib/polyphony/http/http2_adapter.rb +0 -157
  184. data/lib/polyphony/http/rack.rb +0 -25
  185. data/lib/polyphony/http/request.rb +0 -66
  186. data/test/test_ev.rb +0 -110
data/test/test_core.rb CHANGED
@@ -1,13 +1,9 @@
1
- require 'minitest/autorun'
2
- require 'bundler/setup'
3
- require 'polyphony'
1
+ # frozen_string_literal: true
4
2
 
5
- class SpawnTest < MiniTest::Test
6
- def setup
7
- EV.rerun
8
- end
3
+ require_relative 'helper'
9
4
 
10
- def test_that_spawn_returns_a_coprocess
5
+ class SpinTest < MiniTest::Test
6
+ def test_that_spin_returns_a_coprocess
11
7
  result = nil
12
8
  coprocess = spin { result = 42 }
13
9
 
@@ -17,17 +13,17 @@ class SpawnTest < MiniTest::Test
17
13
  assert_equal(42, result)
18
14
  end
19
15
 
20
- def test_that_spawn_accepts_coprocess_argument
16
+ def test_that_spin_accepts_coprocess_argument
21
17
  result = nil
22
18
  coprocess = Polyphony::Coprocess.new { result = 42 }
23
- spin coprocess
19
+ coprocess.run
24
20
 
25
21
  assert_nil(result)
26
22
  suspend
27
23
  assert_equal(42, result)
28
24
  end
29
25
 
30
- def test_that_spawned_coprocess_saves_result
26
+ def test_that_spined_coprocess_saves_result
31
27
  coprocess = spin { 42 }
32
28
 
33
29
  assert_kind_of(Polyphony::Coprocess, coprocess)
@@ -36,20 +32,18 @@ class SpawnTest < MiniTest::Test
36
32
  assert_equal(42, coprocess.result)
37
33
  end
38
34
 
39
- def test_that_spawned_coprocess_can_be_interrupted
40
- result = nil
41
- coprocess = spin { sleep(1); 42 }
42
- EV.next_tick { coprocess.interrupt }
35
+ def test_that_spined_coprocess_can_be_interrupted
36
+ coprocess = spin do
37
+ sleep(1)
38
+ 42
39
+ end
40
+ defer { coprocess.interrupt }
43
41
  suspend
44
42
  assert_nil(coprocess.result)
45
43
  end
46
44
  end
47
45
 
48
46
  class CancelScopeTest < Minitest::Test
49
- def setup
50
- EV.rerun
51
- end
52
-
53
47
  def sleep_with_cancel(ctx, mode = nil)
54
48
  Polyphony::CancelScope.new(mode: mode).call do |c|
55
49
  ctx[:cancel_scope] = c
@@ -60,7 +54,7 @@ class CancelScopeTest < Minitest::Test
60
54
  def test_that_cancel_scope_cancels_coprocess
61
55
  ctx = {}
62
56
  spin do
63
- EV::Timer.new(0.005, 0).start { ctx[:cancel_scope]&.cancel! }
57
+ Gyro::Timer.new(0.005, 0).start { ctx[:cancel_scope]&.cancel! }
64
58
  sleep_with_cancel(ctx, :cancel)
65
59
  rescue Exception => e
66
60
  ctx[:result] = e
@@ -68,7 +62,7 @@ class CancelScopeTest < Minitest::Test
68
62
  assert_nil(ctx[:result])
69
63
  # async operation will only begin on next iteration of event loop
70
64
  assert_nil(ctx[:cancel_scope])
71
-
65
+
72
66
  suspend
73
67
  assert_kind_of(Polyphony::CancelScope, ctx[:cancel_scope])
74
68
  assert_kind_of(Polyphony::Cancel, ctx[:result])
@@ -77,10 +71,10 @@ class CancelScopeTest < Minitest::Test
77
71
  # def test_that_cancel_scope_cancels_async_op_with_stop
78
72
  # ctx = {}
79
73
  # spin do
80
- # EV::Timer.new(0, 0).start { ctx[:cancel_scope].cancel! }
74
+ # Gyro::Timer.new(0, 0).start { ctx[:cancel_scope].cancel! }
81
75
  # sleep_with_cancel(ctx, :stop)
82
76
  # end
83
-
77
+
84
78
  # suspend
85
79
  # assert(ctx[:cancel_scope])
86
80
  # assert_nil(ctx[:result])
@@ -100,45 +94,41 @@ class CancelScopeTest < Minitest::Test
100
94
  assert_equal(:cancelled, result)
101
95
  end
102
96
 
103
- def test_that_cancel_scopes_can_be_nested
104
- inner_result = nil
105
- outer_result = nil
106
- spin do
107
- move_on_after(0.01) do
108
- move_on_after(0.02) do
109
- sleep(1000)
110
- end
111
- inner_result = 42
112
- end
113
- outer_result = 42
114
- end
115
- suspend
116
- assert_nil(inner_result)
117
- assert_equal(42, outer_result)
97
+ # def test_that_cancel_scopes_can_be_nested
98
+ # inner_result = nil
99
+ # outer_result = nil
100
+ # spin do
101
+ # Polyphony::CancelScope.new(timeout: 0.01) do
102
+ # Polyphony::CancelScope.new(timeout: 0.02) do
103
+ # sleep(1000)
104
+ # end
105
+ # inner_result = 42
106
+ # end
107
+ # outer_result = 42
108
+ # end
109
+ # suspend
110
+ # assert_nil(inner_result)
111
+ # assert_equal(42, outer_result)
118
112
 
119
- EV.rerun
113
+ # Polyphony.reset!
120
114
 
121
- outer_result = nil
122
- spin do
123
- move_on_after(0.02) do
124
- move_on_after(0.01) do
125
- sleep(1000)
126
- end
127
- inner_result = 42
128
- end
129
- outer_result = 42
130
- end
131
- suspend
132
- assert_equal(42, inner_result)
133
- assert_equal(42, outer_result)
134
- end
115
+ # outer_result = nil
116
+ # spin do
117
+ # move_on_after(0.02) do
118
+ # move_on_after(0.01) do
119
+ # sleep(1000)
120
+ # end
121
+ # inner_result = 42
122
+ # end
123
+ # outer_result = 42
124
+ # end
125
+ # suspend
126
+ # assert_equal(42, inner_result)
127
+ # assert_equal(42, outer_result)
128
+ # end
135
129
  end
136
130
 
137
131
  class SupervisorTest < MiniTest::Test
138
- def setup
139
- EV.rerun
140
- end
141
-
142
132
  def sleep_and_set(ctx, idx)
143
133
  proc do
144
134
  sleep(0.001 * idx)
@@ -151,7 +141,7 @@ class SupervisorTest < MiniTest::Test
151
141
  (1..3).each { |idx| s.spin sleep_and_set(ctx, idx) }
152
142
  end
153
143
  end
154
-
144
+
155
145
  def test_that_supervisor_waits_for_all_nested_coprocesses_to_complete
156
146
  ctx = {}
157
147
  spin do
@@ -165,7 +155,7 @@ class SupervisorTest < MiniTest::Test
165
155
 
166
156
  def test_that_supervisor_can_add_coprocesses_after_having_started
167
157
  result = []
168
- spin {
158
+ spin do
169
159
  supervisor = Polyphony::Supervisor.new
170
160
  3.times do |i|
171
161
  spin do
@@ -177,8 +167,52 @@ class SupervisorTest < MiniTest::Test
177
167
  end
178
168
  end
179
169
  supervisor.await
180
- }.await
170
+ end.await
181
171
 
182
172
  assert_equal([0, 1, 2], result)
183
173
  end
184
- end
174
+ end
175
+
176
+ class ExceptionTest < MiniTest::Test
177
+ def test_cross_fiber_backtrace
178
+ error = nil
179
+ frames = []
180
+ spin do
181
+ spin do
182
+ spin do
183
+ raise 'foo'
184
+ end
185
+ suspend
186
+ rescue Exception => e
187
+ frames << 2
188
+ raise e
189
+ end
190
+ suspend
191
+ rescue Exception => e
192
+ frames << 3
193
+ raise e
194
+ end
195
+ 4.times { snooze }
196
+ rescue Exception => e
197
+ error = e
198
+ ensure
199
+ assert_kind_of RuntimeError, error
200
+ assert_equal [2, 3], frames
201
+ end
202
+
203
+ def test_cross_fiber_backtrace_with_dead_calling_fiber
204
+ error = nil
205
+ spin do
206
+ spin do
207
+ spin do
208
+ raise 'foo'
209
+ end
210
+ end
211
+ end
212
+ 4.times { snooze }
213
+ rescue Exception => e
214
+ error = e
215
+ ensure
216
+ assert_kind_of RuntimeError, error
217
+ end
218
+ end
data/test/test_gyro.rb ADDED
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class RunTest < Minitest::Test
6
+ def test_that_run_loop_returns_immediately_if_no_watchers
7
+ t0 = Time.now
8
+ suspend
9
+ t1 = Time.now
10
+ assert((t1 - t0) < 0.01)
11
+ end
12
+ end
13
+
14
+ class IdleTest < MiniTest::Test
15
+ def test_defer
16
+ values = []
17
+ defer { values << 1 }
18
+ defer { values << 2 }
19
+ defer { values << 3 }
20
+ suspend
21
+
22
+ assert_equal [1, 2, 3], values
23
+ end
24
+
25
+ def test_schedule
26
+ values = []
27
+ f = Fiber.new do
28
+ values << :foo
29
+ # We *have* to suspend the fiber in order to yield to the reactor,
30
+ # otherwise control will transfer back to root fiber.
31
+ suspend
32
+ end
33
+ assert_equal [], values
34
+ f.schedule
35
+ suspend
36
+
37
+ assert_equal [:foo], values
38
+ end
39
+
40
+ def test_suspend
41
+ values = []
42
+ Fiber.new do
43
+ values << :foo
44
+ suspend
45
+ end.schedule
46
+ suspend
47
+
48
+ assert_equal [:foo], values
49
+ end
50
+
51
+ def test_schedule_and_suspend
52
+ values = []
53
+ 3.times.map do |i|
54
+ Fiber.new do
55
+ values << i
56
+ suspend
57
+ end.schedule
58
+ end
59
+ suspend
60
+
61
+ assert_equal [0, 1, 2], values
62
+ end
63
+
64
+ def test_snooze
65
+ values = []
66
+ 3.times.map do |i|
67
+ Fiber.new do
68
+ 3.times do
69
+ snooze
70
+ values << i
71
+ end
72
+ suspend
73
+ end.schedule
74
+ end
75
+ suspend
76
+
77
+ assert_equal [0, 1, 2, 0, 1, 2, 0, 1, 2], values
78
+ end
79
+
80
+ def test_break
81
+ values = []
82
+ Fiber.new do
83
+ values << :foo
84
+ snooze
85
+ # here will never be reached
86
+ values << :bar
87
+ suspend
88
+ end.schedule
89
+
90
+ Fiber.new do
91
+ Gyro.break
92
+ end.schedule
93
+
94
+ suspend
95
+
96
+ assert_equal [:foo], values
97
+ end
98
+
99
+ def test_start
100
+ values = []
101
+ f1 = Fiber.new do
102
+ values << :foo
103
+ snooze
104
+ values << :bar
105
+ suspend
106
+ end.schedule
107
+
108
+ f2 = Fiber.new do
109
+ Gyro.break
110
+ values << :restarted
111
+ snooze
112
+ values << :baz
113
+ end.schedule
114
+
115
+ suspend
116
+
117
+ Gyro.start
118
+ f2.schedule
119
+ f1.schedule
120
+ suspend
121
+
122
+ assert_equal %i[foo restarted bar baz], values
123
+ end
124
+
125
+ def test_restart
126
+ values = []
127
+ Fiber.new do
128
+ values << :foo
129
+ snooze
130
+ # this part will not be reached, as f
131
+ values << :bar
132
+ suspend
133
+ end.schedule
134
+
135
+ Fiber.new do
136
+ Gyro.restart
137
+
138
+ # control is transfer to the fiber that called Gyro.restart
139
+ values << :restarted
140
+ snooze
141
+ values << :baz
142
+ end.schedule
143
+
144
+ suspend
145
+
146
+ assert_equal %i[foo restarted baz], values
147
+ end
148
+ end
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'polyphony/http'
5
+
6
+ class String
7
+ def http_lines
8
+ gsub "\n", "\r\n"
9
+ end
10
+ end
11
+
12
+ class IO
13
+ # Creates two mockup sockets for simulating server-client communication
14
+ def self.server_client_mockup
15
+ server_in, client_out = IO.pipe
16
+ client_in, server_out = IO.pipe
17
+
18
+ server_connection = mockup_connection(server_in, server_out, client_out)
19
+ client_connection = mockup_connection(client_in, client_out, server_out)
20
+
21
+ [server_connection, client_connection]
22
+ end
23
+
24
+ def self.mockup_connection(input, output, output2)
25
+ eg(
26
+ :read => ->(*args) { input.read(*args) },
27
+ :readpartial => ->(*args) { input.readpartial(*args) },
28
+ :<< => ->(*args) { output.write(*args) },
29
+ :write => ->(*args) { output.write(*args) },
30
+ :close => -> { output.close },
31
+ :eof? => -> { output2.closed? }
32
+ )
33
+ end
34
+ end
35
+
36
+ class HTTP1ServerTest < MiniTest::Test
37
+ def teardown
38
+ @server&.interrupt if @server&.alive?
39
+ snooze
40
+ super
41
+ end
42
+
43
+ def spin_server(opts = {}, &handler)
44
+ server_connection, client_connection = IO.server_client_mockup
45
+ coproc = spin do
46
+ Polyphony::HTTP::Server.client_loop(server_connection, opts, &handler)
47
+ end
48
+ [coproc, client_connection, server_connection]
49
+ end
50
+
51
+ def test_that_server_uses_content_length_in_http_1_0
52
+ @server, connection = spin_server do |req|
53
+ req.respond('Hello, world!', {})
54
+ end
55
+
56
+ # using HTTP 1.0, server should close connection after responding
57
+ connection << "GET / HTTP/1.0\r\n\r\n"
58
+
59
+ response = connection.readpartial(8192)
60
+ expected = <<~HTTP.chomp.http_lines
61
+ HTTP/1.0 200
62
+ Content-Length: 13
63
+
64
+ Hello, world!
65
+ HTTP
66
+ assert_equal(expected, response)
67
+ end
68
+
69
+ def test_that_server_uses_chunked_encoding_in_http_1_1
70
+ @server, connection = spin_server do |req|
71
+ req.respond('Hello, world!')
72
+ end
73
+
74
+ # using HTTP 1.0, server should close connection after responding
75
+ connection << "GET / HTTP/1.1\r\n\r\n"
76
+
77
+ response = connection.readpartial(8192)
78
+ expected = <<~HTTP.http_lines
79
+ HTTP/1.1 200
80
+ Transfer-Encoding: chunked
81
+
82
+ d
83
+ Hello, world!
84
+ 0
85
+
86
+ HTTP
87
+ assert_equal(expected, response)
88
+ end
89
+
90
+ def test_that_server_maintains_connection_when_using_keep_alives
91
+ puts 'test_that_server_maintains_connection_when_using_keep_alives'
92
+ @server, connection = spin_server do |req|
93
+ req.respond('Hi', {})
94
+ end
95
+
96
+ connection << "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
97
+ response = connection.readpartial(8192)
98
+ assert !connection.eof?
99
+ assert_equal("HTTP/1.0 200\r\nContent-Length: 2\r\n\r\nHi", response)
100
+
101
+ connection << "GET / HTTP/1.1\r\n\r\n"
102
+ response = connection.readpartial(8192)
103
+ assert !connection.eof?
104
+ expected = <<~HTTP.http_lines
105
+ HTTP/1.1 200
106
+ Transfer-Encoding: chunked
107
+
108
+ 2
109
+ Hi
110
+ 0
111
+
112
+ HTTP
113
+ assert_equal(expected, response)
114
+
115
+ connection << "GET / HTTP/1.0\r\n\r\n"
116
+ response = connection.readpartial(8192)
117
+ assert connection.eof?
118
+ assert_equal("HTTP/1.0 200\r\nContent-Length: 2\r\n\r\nHi", response)
119
+ end
120
+
121
+ def test_pipelining_client
122
+ @server, connection = spin_server do |req|
123
+ if req.headers['Foo'] == 'bar'
124
+ req.respond('Hello, foobar!', {})
125
+ else
126
+ req.respond('Hello, world!', {})
127
+ end
128
+ end
129
+
130
+ connection << "GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nFoo: bar\r\n\r\n"
131
+ response = connection.readpartial(8192)
132
+
133
+ expected = <<~HTTP.http_lines
134
+ HTTP/1.1 200
135
+ Transfer-Encoding: chunked
136
+
137
+ d
138
+ Hello, world!
139
+ 0
140
+
141
+ HTTP/1.1 200
142
+ Transfer-Encoding: chunked
143
+
144
+ e
145
+ Hello, foobar!
146
+ 0
147
+
148
+ HTTP
149
+ assert_equal(expected, response)
150
+ end
151
+
152
+ def test_body_chunks
153
+ chunks = []
154
+ request = nil
155
+ @server, connection = spin_server do |req|
156
+ request = req
157
+ req.send_headers
158
+ req.each_chunk do |c|
159
+ chunks << c
160
+ req << c.upcase
161
+ end
162
+ req.finish
163
+ end
164
+
165
+ connection << <<~HTTP.http_lines
166
+ POST / HTTP/1.1
167
+ Transfer-Encoding: chunked
168
+
169
+ 6
170
+ foobar
171
+ HTTP
172
+ 2.times { snooze }
173
+ assert request
174
+ assert_equal %w[foobar], chunks
175
+ assert !request.complete?
176
+
177
+ connection << "6\r\nbazbud\r\n"
178
+ snooze
179
+ assert_equal %w[foobar bazbud], chunks
180
+ assert !request.complete?
181
+
182
+ connection << "0\r\n\r\n"
183
+ snooze
184
+ assert_equal %w[foobar bazbud], chunks
185
+ assert request.complete?
186
+
187
+ 2.times { snooze }
188
+
189
+ response = connection.readpartial(8192)
190
+
191
+ expected = <<~HTTP.http_lines
192
+ HTTP/1.1 200
193
+ Transfer-Encoding: chunked
194
+
195
+ 6
196
+ FOOBAR
197
+ 6
198
+ BAZBUD
199
+ 0
200
+
201
+ HTTP
202
+ assert_equal(expected, response)
203
+ end
204
+
205
+ def test_upgrade
206
+ done = nil
207
+
208
+ opts = {
209
+ upgrade: {
210
+ echo: lambda do |conn, _headers|
211
+ conn << <<~HTTP.http_lines
212
+ HTTP/1.1 101 Switching Protocols
213
+ Upgrade: echo
214
+ Connection: Upgrade
215
+
216
+ HTTP
217
+
218
+ while (data = conn.readpartial(8192))
219
+ conn << data
220
+ snooze
221
+ end
222
+ done = true
223
+ end
224
+ }
225
+ }
226
+
227
+ @server, connection = spin_server(opts) do |req|
228
+ req.respond('Hi')
229
+ end
230
+
231
+ connection << "GET / HTTP/1.1\r\n\r\n"
232
+ response = connection.readpartial(8192)
233
+ assert !connection.eof?
234
+ expected = <<~HTTP.http_lines
235
+ HTTP/1.1 200
236
+ Transfer-Encoding: chunked
237
+
238
+ 2
239
+ Hi
240
+ 0
241
+
242
+ HTTP
243
+ assert_equal(expected, response)
244
+
245
+ connection << <<~HTTP.http_lines
246
+ GET / HTTP/1.1
247
+ Upgrade: echo
248
+ Connection: upgrade
249
+
250
+ HTTP
251
+
252
+ snooze
253
+ response = connection.readpartial(8192)
254
+ assert !connection.eof?
255
+ expected = <<~HTTP.http_lines
256
+ HTTP/1.1 101 Switching Protocols
257
+ Upgrade: echo
258
+ Connection: Upgrade
259
+
260
+ HTTP
261
+ assert_equal(expected, response)
262
+
263
+ assert !done
264
+
265
+ connection << 'foo'
266
+ assert_equal 'foo', connection.readpartial(8192)
267
+
268
+ connection << 'bar'
269
+ assert_equal 'bar', connection.readpartial(8192)
270
+
271
+ connection.close
272
+ assert !done
273
+ snooze
274
+ assert done
275
+ end
276
+
277
+ def test_big_download
278
+ chunk_size = 100_000
279
+ chunk_count = 10
280
+ chunk = '*' * chunk_size
281
+ @server, connection = spin_server do |req|
282
+ req.send_headers
283
+ chunk_count.times do
284
+ req << chunk
285
+ snooze
286
+ end
287
+ req.finish
288
+ req.adapter.close
289
+ end
290
+
291
+ response = +''
292
+ count = 0
293
+
294
+ connection << "GET / HTTP/1.1\r\n\r\n"
295
+ while (data = connection.readpartial(chunk_size * 2))
296
+ response << data
297
+ count += 1
298
+ snooze
299
+ end
300
+
301
+ chunks = "#{chunk_size.to_s(16)}\n#{'*' * chunk_size}\n" * chunk_count
302
+ expected = <<~HTTP.http_lines
303
+ HTTP/1.1 200
304
+ Transfer-Encoding: chunked
305
+
306
+ #{chunks}0
307
+
308
+ HTTP
309
+
310
+ assert_equal expected, response
311
+ assert_equal chunk_count * 2 + 1, count
312
+ end
313
+ end