polyphony 0.19 → 0.20

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 (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