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.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.rubocop.yml +87 -1
- data/CHANGELOG.md +35 -0
- data/Gemfile.lock +17 -6
- data/README.md +200 -139
- data/Rakefile +4 -4
- data/TODO.md +35 -7
- data/bin/poly +11 -0
- data/docs/getting-started/getting-started.md +1 -1
- data/docs/summary.md +3 -0
- data/docs/technical-overview/exception-handling.md +94 -0
- data/docs/technical-overview/fiber-scheduling.md +99 -0
- data/examples/core/cancel.rb +8 -4
- data/examples/core/channel_echo.rb +18 -17
- data/examples/core/defer.rb +12 -0
- data/examples/core/enumerator.rb +4 -4
- data/examples/core/fiber_error.rb +9 -0
- data/examples/core/fiber_error_with_backtrace.rb +73 -0
- data/examples/core/fork.rb +6 -6
- data/examples/core/genserver.rb +16 -8
- data/examples/core/lock.rb +3 -3
- data/examples/core/move_on.rb +4 -3
- data/examples/core/move_on_twice.rb +5 -5
- data/examples/core/move_on_with_ensure.rb +8 -11
- data/examples/core/move_on_with_value.rb +14 -0
- data/examples/core/{multiple_spawn.rb → multiple_spin.rb} +5 -5
- data/examples/core/nested_cancel.rb +5 -5
- data/examples/core/{nested_multiple_spawn.rb → nested_multiple_spin.rb} +6 -6
- data/examples/core/nested_spin.rb +17 -0
- data/examples/core/pingpong.rb +21 -0
- data/examples/core/pulse.rb +4 -5
- data/examples/core/resource.rb +6 -4
- data/examples/core/resource_cancel.rb +6 -9
- data/examples/core/resource_delegate.rb +3 -3
- data/examples/core/sleep.rb +3 -3
- data/examples/core/sleep_spin.rb +19 -0
- data/examples/core/snooze.rb +32 -0
- data/examples/core/spin.rb +14 -0
- data/examples/core/{spawn_cancel.rb → spin_cancel.rb} +6 -7
- data/examples/core/spin_error.rb +17 -0
- data/examples/core/spin_error_backtrace.rb +30 -0
- data/examples/core/spin_uncaught_error.rb +15 -0
- data/examples/core/supervisor.rb +8 -8
- data/examples/core/supervisor_with_cancel_scope.rb +7 -7
- data/examples/core/supervisor_with_error.rb +8 -8
- data/examples/core/supervisor_with_manual_move_on.rb +6 -7
- data/examples/core/suspend.rb +13 -0
- data/examples/core/thread.rb +1 -1
- data/examples/core/thread_cancel.rb +9 -11
- data/examples/core/thread_pool.rb +18 -14
- data/examples/core/throttle.rb +7 -7
- data/examples/core/timeout.rb +3 -3
- data/examples/fs/read.rb +7 -9
- data/examples/http/config.ru +7 -3
- data/examples/http/cuba.ru +22 -0
- data/examples/http/happy_eyeballs.rb +6 -4
- data/examples/http/http_client.rb +1 -1
- data/examples/http/http_get.rb +1 -1
- data/examples/http/http_parse_experiment.rb +21 -16
- data/examples/http/http_proxy.rb +28 -26
- data/examples/http/http_server.rb +10 -10
- data/examples/http/http_server_forked.rb +6 -5
- data/examples/http/http_server_throttled.rb +3 -3
- data/examples/http/http_ws_server.rb +11 -11
- data/examples/http/https_raw_client.rb +1 -1
- data/examples/http/https_server.rb +8 -8
- data/examples/http/https_wss_server.rb +13 -11
- data/examples/http/rack_server.rb +2 -2
- data/examples/http/rack_server_https.rb +4 -4
- data/examples/http/rack_server_https_forked.rb +5 -5
- data/examples/http/websocket_secure_server.rb +6 -6
- data/examples/http/websocket_server.rb +5 -5
- data/examples/interfaces/pg_client.rb +4 -4
- data/examples/interfaces/pg_pool.rb +13 -6
- data/examples/interfaces/pg_transaction.rb +5 -4
- data/examples/interfaces/redis_channels.rb +15 -11
- data/examples/interfaces/redis_client.rb +2 -2
- data/examples/interfaces/redis_pubsub.rb +2 -1
- data/examples/interfaces/redis_pubsub_perf.rb +13 -9
- data/examples/io/backticks.rb +11 -0
- data/examples/io/cat.rb +4 -5
- data/examples/io/echo_client.rb +9 -4
- data/examples/io/echo_client_from_stdin.rb +20 -0
- data/examples/io/echo_pipe.rb +7 -8
- data/examples/io/echo_server.rb +8 -6
- data/examples/io/echo_server_with_timeout.rb +13 -10
- data/examples/io/echo_stdin.rb +3 -3
- data/examples/io/httparty.rb +2 -2
- data/examples/io/httparty_multi.rb +8 -4
- data/examples/io/httparty_threaded.rb +6 -2
- data/examples/io/io_read.rb +2 -2
- data/examples/io/irb.rb +16 -4
- data/examples/io/net-http.rb +3 -3
- data/examples/io/open.rb +17 -0
- data/examples/io/system.rb +3 -3
- data/examples/io/tcpserver.rb +15 -0
- data/examples/io/tcpsocket.rb +6 -5
- data/examples/performance/multi_snooze.rb +29 -0
- data/examples/performance/{perf_snooze.rb → snooze.rb} +7 -5
- data/examples/performance/snooze_raw.rb +39 -0
- data/ext/gyro/async.c +165 -0
- data/ext/gyro/child.c +167 -0
- data/ext/{ev → gyro}/extconf.rb +4 -3
- data/ext/gyro/gyro.c +316 -0
- data/ext/{ev/ev.h → gyro/gyro.h} +12 -7
- data/ext/gyro/gyro_ext.c +23 -0
- data/ext/{ev → gyro}/io.c +65 -57
- data/ext/{ev → gyro}/libev.h +0 -0
- data/ext/gyro/signal.c +117 -0
- data/ext/{ev → gyro}/socket.c +61 -6
- data/ext/gyro/timer.c +199 -0
- data/ext/libev/Changes +35 -0
- data/ext/libev/README +2 -1
- data/ext/libev/ev.c +213 -151
- data/ext/libev/ev.h +95 -88
- data/ext/libev/ev_epoll.c +26 -15
- data/ext/libev/ev_kqueue.c +11 -5
- data/ext/libev/ev_linuxaio.c +642 -0
- data/ext/libev/ev_poll.c +13 -8
- data/ext/libev/ev_port.c +5 -2
- data/ext/libev/ev_vars.h +14 -3
- data/ext/libev/ev_wrap.h +16 -0
- data/lib/ev_ext.bundle +0 -0
- data/lib/polyphony.rb +46 -50
- data/lib/polyphony/auto_run.rb +12 -0
- data/lib/polyphony/core/cancel_scope.rb +11 -7
- data/lib/polyphony/core/channel.rb +16 -9
- data/lib/polyphony/core/coprocess.rb +101 -51
- data/lib/polyphony/core/exceptions.rb +14 -12
- data/lib/polyphony/core/resource_pool.rb +21 -8
- data/lib/polyphony/core/supervisor.rb +10 -5
- data/lib/polyphony/core/sync.rb +7 -6
- data/lib/polyphony/core/thread.rb +4 -4
- data/lib/polyphony/core/thread_pool.rb +4 -4
- data/lib/polyphony/core/throttler.rb +6 -4
- data/lib/polyphony/extensions/core.rb +253 -0
- data/lib/polyphony/extensions/io.rb +28 -16
- data/lib/polyphony/extensions/openssl.rb +2 -1
- data/lib/polyphony/extensions/socket.rb +47 -52
- data/lib/polyphony/http.rb +4 -3
- data/lib/polyphony/http/agent.rb +68 -57
- data/lib/polyphony/http/server.rb +5 -5
- data/lib/polyphony/http/server/http1.rb +268 -0
- data/lib/polyphony/http/server/http2.rb +62 -0
- data/lib/polyphony/http/server/http2_stream.rb +104 -0
- data/lib/polyphony/http/server/rack.rb +64 -0
- data/lib/polyphony/http/server/request.rb +119 -0
- data/lib/polyphony/net.rb +26 -15
- data/lib/polyphony/postgres.rb +17 -13
- data/lib/polyphony/redis.rb +16 -15
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony/websocket.rb +11 -4
- data/polyphony.gemspec +13 -9
- data/test/eg.rb +27 -0
- data/test/helper.rb +25 -0
- data/test/run.rb +5 -0
- data/test/test_async.rb +33 -0
- data/test/test_coprocess.rb +239 -77
- data/test/test_core.rb +95 -61
- data/test/test_gyro.rb +148 -0
- data/test/test_http_server.rb +313 -0
- data/test/test_io.rb +79 -27
- data/test/test_kernel.rb +22 -12
- data/test/test_signal.rb +36 -0
- data/test/test_timer.rb +24 -0
- metadata +89 -33
- data/examples/core/nested_async.rb +0 -17
- data/examples/core/next_tick.rb +0 -12
- data/examples/core/sleep_spawn.rb +0 -19
- data/examples/core/spawn.rb +0 -14
- data/examples/core/spawn_error.rb +0 -28
- data/examples/performance/perf_multi_snooze.rb +0 -21
- data/ext/ev/async.c +0 -168
- data/ext/ev/child.c +0 -169
- data/ext/ev/ev_ext.c +0 -23
- data/ext/ev/ev_module.c +0 -242
- data/ext/ev/signal.c +0 -119
- data/ext/ev/timer.c +0 -197
- data/lib/polyphony/core/fiber_pool.rb +0 -98
- data/lib/polyphony/extensions/kernel.rb +0 -169
- data/lib/polyphony/http/http1_adapter.rb +0 -254
- data/lib/polyphony/http/http2_adapter.rb +0 -157
- data/lib/polyphony/http/rack.rb +0 -25
- data/lib/polyphony/http/request.rb +0 -66
- data/test/test_ev.rb +0 -110
data/test/test_core.rb
CHANGED
@@ -1,13 +1,9 @@
|
|
1
|
-
|
2
|
-
require 'bundler/setup'
|
3
|
-
require 'polyphony'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
def setup
|
7
|
-
EV.rerun
|
8
|
-
end
|
3
|
+
require_relative 'helper'
|
9
4
|
|
10
|
-
|
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
|
16
|
+
def test_that_spin_accepts_coprocess_argument
|
21
17
|
result = nil
|
22
18
|
coprocess = Polyphony::Coprocess.new { result = 42 }
|
23
|
-
|
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
|
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
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
113
|
+
# Polyphony.reset!
|
120
114
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
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
|