polyphony 0.50.0 → 0.53.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +30 -0
  4. data/Gemfile.lock +7 -68
  5. data/TODO.md +37 -6
  6. data/examples/core/forking.rb +2 -2
  7. data/examples/io/echo_server.rb +1 -0
  8. data/examples/io/tcp_proxy.rb +2 -2
  9. data/ext/polyphony/backend_common.h +57 -7
  10. data/ext/polyphony/backend_io_uring.c +232 -49
  11. data/ext/polyphony/backend_io_uring_context.c +1 -0
  12. data/ext/polyphony/backend_io_uring_context.h +1 -0
  13. data/ext/polyphony/backend_libev.c +355 -34
  14. data/ext/polyphony/event.c +1 -1
  15. data/ext/polyphony/extconf.rb +9 -2
  16. data/ext/polyphony/polyphony.c +102 -0
  17. data/ext/polyphony/polyphony.h +32 -2
  18. data/ext/polyphony/polyphony_ext.c +3 -0
  19. data/ext/polyphony/queue.c +1 -1
  20. data/ext/polyphony/runqueue.c +1 -1
  21. data/ext/polyphony/socket_extensions.c +33 -0
  22. data/ext/polyphony/thread.c +8 -2
  23. data/lib/polyphony/adapters/irb.rb +1 -1
  24. data/lib/polyphony/adapters/mysql2.rb +1 -1
  25. data/lib/polyphony/adapters/postgres.rb +5 -5
  26. data/lib/polyphony/adapters/process.rb +4 -4
  27. data/lib/polyphony/core/global_api.rb +5 -5
  28. data/lib/polyphony/core/sync.rb +1 -1
  29. data/lib/polyphony/core/throttler.rb +1 -1
  30. data/lib/polyphony/core/timer.rb +2 -2
  31. data/lib/polyphony/extensions/core.rb +1 -1
  32. data/lib/polyphony/extensions/io.rb +21 -22
  33. data/lib/polyphony/extensions/openssl.rb +6 -6
  34. data/lib/polyphony/extensions/socket.rb +56 -47
  35. data/lib/polyphony/version.rb +1 -1
  36. data/polyphony.gemspec +6 -5
  37. data/test/helper.rb +1 -1
  38. data/test/stress.rb +2 -0
  39. data/test/test_backend.rb +152 -5
  40. data/test/test_global_api.rb +2 -2
  41. data/test/test_io.rb +84 -1
  42. data/test/test_kernel.rb +1 -1
  43. data/test/test_signal.rb +1 -1
  44. data/test/test_socket.rb +61 -0
  45. data/test/test_thread.rb +4 -0
  46. data/test/test_timer.rb +1 -1
  47. metadata +19 -60
@@ -12,7 +12,7 @@ module Polyphony
12
12
  def call
13
13
  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
14
14
  delta = @next_time - now
15
- Thread.current.backend.sleep(delta) if delta > 0
15
+ Polyphony.backend_sleep(delta) if delta > 0
16
16
  yield self
17
17
 
18
18
  while true
@@ -18,7 +18,7 @@ module Polyphony
18
18
  interval: duration,
19
19
  target_stamp: now + duration
20
20
  }
21
- Thread.current.backend.wait_event(true)
21
+ Polyphony.backend_wait_event(true)
22
22
  ensure
23
23
  @timeouts.delete(fiber)
24
24
  end
@@ -38,7 +38,7 @@ module Polyphony
38
38
  recurring: true
39
39
  }
40
40
  while true
41
- Thread.current.backend.wait_event(true)
41
+ Polyphony.backend_wait_event(true)
42
42
  yield
43
43
  end
44
44
  ensure
@@ -55,7 +55,7 @@ module ::Process
55
55
  class << self
56
56
  alias_method :orig_detach, :detach
57
57
  def detach(pid)
58
- fiber = spin { Thread.current.backend.waitpid(pid) }
58
+ fiber = spin { Polyphony.backend_waitpid(pid) }
59
59
  fiber.define_singleton_method(:pid) { pid }
60
60
  fiber
61
61
  end
@@ -101,7 +101,7 @@ class ::IO
101
101
  return @read_buffer.slice!(0) if @read_buffer && !@read_buffer.empty?
102
102
 
103
103
  @read_buffer ||= +''
104
- Thread.current.backend.read(self, @read_buffer, 8192, false)
104
+ Polyphony.backend_read(self, @read_buffer, 8192, false)
105
105
  return @read_buffer.slice!(0) if !@read_buffer.empty?
106
106
 
107
107
  nil
@@ -110,7 +110,7 @@ class ::IO
110
110
  alias_method :orig_read, :read
111
111
  def read(len = nil)
112
112
  @read_buffer ||= +''
113
- result = Thread.current.backend.read(self, @read_buffer, len, true)
113
+ result = Polyphony.backend_read(self, @read_buffer, len, true)
114
114
  return nil unless result
115
115
 
116
116
  already_read = @read_buffer
@@ -120,7 +120,7 @@ class ::IO
120
120
 
121
121
  alias_method :orig_readpartial, :read
122
122
  def readpartial(len, str = +'')
123
- result = Thread.current.backend.read(self, str, len, false)
123
+ result = Polyphony.backend_read(self, str, len, false)
124
124
  raise EOFError unless result
125
125
 
126
126
  result
@@ -128,12 +128,12 @@ class ::IO
128
128
 
129
129
  alias_method :orig_write, :write
130
130
  def write(str, *args)
131
- Thread.current.backend.write(self, str, *args)
131
+ Polyphony.backend_write(self, str, *args)
132
132
  end
133
133
 
134
134
  alias_method :orig_write_chevron, :<<
135
135
  def <<(str)
136
- Thread.current.backend.write(self, str)
136
+ Polyphony.backend_write(self, str)
137
137
  self
138
138
  end
139
139
 
@@ -217,30 +217,21 @@ class ::IO
217
217
  end
218
218
 
219
219
  def read_loop(&block)
220
- Thread.current.backend.read_loop(self, &block)
220
+ Polyphony.backend_read_loop(self, &block)
221
221
  end
222
222
 
223
- # alias_method :orig_read, :read
224
- # def read(length = nil, outbuf = nil)
225
- # if length
226
- # return outbuf ? readpartial(length) : readpartial(length, outbuf)
227
- # end
228
-
229
- # until eof?
230
- # outbuf ||= +''
231
- # outbuf << readpartial(8192)
232
- # end
233
- # outbuf
234
- # end
223
+ def feed_loop(receiver, method = :call, &block)
224
+ Polyphony.backend_feed_loop(self, receiver, method, &block)
225
+ end
235
226
 
236
227
  def wait_readable(timeout = nil)
237
228
  if timeout
238
229
  move_on_after(timeout) do
239
- Thread.current.backend.wait_io(self, false)
230
+ Polyphony.backend_wait_io(self, false)
240
231
  self
241
232
  end
242
233
  else
243
- Thread.current.backend.wait_io(self, false)
234
+ Polyphony.backend_wait_io(self, false)
244
235
  self
245
236
  end
246
237
  end
@@ -248,12 +239,20 @@ class ::IO
248
239
  def wait_writable(timeout = nil)
249
240
  if timeout
250
241
  move_on_after(timeout) do
251
- Thread.current.backend.wait_io(self, true)
242
+ Polyphony.backend_wait_io(self, true)
252
243
  self
253
244
  end
254
245
  else
255
- Thread.current.backend.wait_io(self, true)
246
+ Polyphony.backend_wait_io(self, true)
256
247
  self
257
248
  end
258
249
  end
250
+
251
+ def splice(src, maxlen)
252
+ Polyphony.backend_splice(src, self, maxlen)
253
+ end
254
+
255
+ def splice_to_eof(src, chunksize = 8192)
256
+ Polyphony.backend_splice_to_eof(src, self, chunksize)
257
+ end
259
258
  end
@@ -28,8 +28,8 @@ class ::OpenSSL::SSL::SSLSocket
28
28
  while true
29
29
  result = accept_nonblock(exception: false)
30
30
  case result
31
- when :wait_readable then Thread.current.backend.wait_io(io, false)
32
- when :wait_writable then Thread.current.backend.wait_io(io, true)
31
+ when :wait_readable then Polyphony.backend_wait_io(io, false)
32
+ when :wait_writable then Polyphony.backend_wait_io(io, true)
33
33
  else
34
34
  return result
35
35
  end
@@ -46,8 +46,8 @@ class ::OpenSSL::SSL::SSLSocket
46
46
  def sysread(maxlen, buf = +'')
47
47
  while true
48
48
  case (result = read_nonblock(maxlen, buf, exception: false))
49
- when :wait_readable then Thread.current.backend.wait_io(io, false)
50
- when :wait_writable then Thread.current.backend.wait_io(io, true)
49
+ when :wait_readable then Polyphony.backend_wait_io(io, false)
50
+ when :wait_writable then Polyphony.backend_wait_io(io, true)
51
51
  else return result
52
52
  end
53
53
  end
@@ -57,8 +57,8 @@ class ::OpenSSL::SSL::SSLSocket
57
57
  def syswrite(buf)
58
58
  while true
59
59
  case (result = write_nonblock(buf, exception: false))
60
- when :wait_readable then Thread.current.backend.wait_io(io, false)
61
- when :wait_writable then Thread.current.backend.wait_io(io, true)
60
+ when :wait_readable then Polyphony.backend_wait_io(io, false)
61
+ when :wait_writable then Polyphony.backend_wait_io(io, true)
62
62
  else
63
63
  return result
64
64
  end
@@ -8,57 +8,60 @@ require_relative '../core/thread_pool'
8
8
  # Socket overrides (eventually rewritten in C)
9
9
  class ::Socket
10
10
  def accept
11
- Thread.current.backend.accept(self, TCPSocket)
11
+ Polyphony.backend_accept(self, TCPSocket)
12
12
  end
13
13
 
14
14
  def accept_loop(&block)
15
- Thread.current.backend.accept_loop(self, TCPSocket, &block)
15
+ Polyphony.backend_accept_loop(self, TCPSocket, &block)
16
16
  end
17
17
 
18
18
  NO_EXCEPTION = { exception: false }.freeze
19
19
 
20
20
  def connect(addr)
21
21
  addr = Addrinfo.new(addr) if addr.is_a?(String)
22
- Thread.current.backend.connect(self, addr.ip_address, addr.ip_port)
22
+ Polyphony.backend_connect(self, addr.ip_address, addr.ip_port)
23
23
  end
24
24
 
25
25
  def recv(maxlen, flags = 0, outbuf = nil)
26
- Thread.current.backend.recv(self, outbuf || +'', maxlen)
26
+ Polyphony.backend_recv(self, outbuf || +'', maxlen)
27
27
  end
28
28
 
29
29
  def recv_loop(&block)
30
- Thread.current.backend.recv_loop(self, &block)
30
+ Polyphony.backend_recv_loop(self, &block)
31
31
  end
32
32
  alias_method :read_loop, :recv_loop
33
33
 
34
+ def feed_loop(receiver, method = :call, &block)
35
+ Polyphony.backend_recv_feed_loop(self, receiver, method, &block)
36
+ end
37
+
34
38
  def recvfrom(maxlen, flags = 0)
35
39
  @read_buffer ||= +''
36
40
  while true
37
41
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
38
42
  case result
39
43
  when nil then raise IOError
40
- when :wait_readable then Thread.current.backend.wait_io(self, false)
44
+ when :wait_readable then Polyphony.backend_wait_io(self, false)
41
45
  else
42
46
  return result
43
47
  end
44
48
  end
45
49
  end
46
50
 
47
- def send(mesg, flags = 0)
48
- Thread.current.backend.send(self, mesg)
49
- end
51
+ # def send(mesg, flags)
52
+ # Polyphony.backend_send(self, mesg, flags)
53
+ # end
50
54
 
51
- def write(str, *args)
52
- if args.empty?
53
- Thread.current.backend.send(self, str)
54
- else
55
- Thread.current.backend.send(self, str + args.join)
56
- end
57
- end
58
- alias_method :<<, :write
55
+ # def write(*args)
56
+ # Polyphony.backend_sendv(self, args, 0)
57
+ # end
58
+
59
+ # def <<(mesg)
60
+ # Polyphony.backend_send(self, mesg, 0)
61
+ # end
59
62
 
60
63
  def readpartial(maxlen, str = +'')
61
- Thread.current.backend.recv(self, str, maxlen)
64
+ Polyphony.backend_recv(self, str, maxlen)
62
65
  end
63
66
 
64
67
  ZERO_LINGER = [0, 0].pack('ii').freeze
@@ -138,30 +141,33 @@ class ::TCPSocket
138
141
  end
139
142
 
140
143
  def recv(maxlen, flags = 0, outbuf = nil)
141
- Thread.current.backend.recv(self, outbuf || +'', maxlen)
144
+ Polyphony.backend_recv(self, outbuf || +'', maxlen)
142
145
  end
143
146
 
144
147
  def recv_loop(&block)
145
- Thread.current.backend.recv_loop(self, &block)
148
+ Polyphony.backend_recv_loop(self, &block)
146
149
  end
147
150
  alias_method :read_loop, :recv_loop
148
151
 
149
- def send(mesg, flags = 0)
150
- Thread.current.backend.send(self, mesg)
152
+ def feed_loop(receiver, method = :call, &block)
153
+ Polyphony.backend_recv_feed_loop(self, receiver, method, &block)
151
154
  end
152
155
 
153
- def write(str, *args)
154
- if args.empty?
155
- Thread.current.backend.send(self, str)
156
- else
157
- Thread.current.backend.send(self, str + args.join)
158
- end
159
- end
160
- alias_method :<<, :write
156
+ # def send(mesg, flags)
157
+ # Polyphony.backend_send(self, mesg, flags)
158
+ # end
159
+
160
+ # def write(*args)
161
+ # Polyphony.backend_sendv(self, args, 0)
162
+ # end
163
+
164
+ # def <<(mesg)
165
+ # Polyphony.backend_send(self, mesg, 0)
166
+ # end
161
167
 
162
168
  def readpartial(maxlen, str = nil)
163
169
  @read_buffer ||= +''
164
- result = Thread.current.backend.recv(self, @read_buffer, maxlen)
170
+ result = Polyphony.backend_recv(self, @read_buffer, maxlen)
165
171
  raise EOFError unless result
166
172
 
167
173
  if str
@@ -192,12 +198,12 @@ class ::TCPServer
192
198
 
193
199
  alias_method :orig_accept, :accept
194
200
  def accept
195
- Thread.current.backend.accept(@io, TCPSocket)
201
+ Polyphony.backend_accept(@io, TCPSocket)
196
202
  # @io.accept
197
203
  end
198
204
 
199
205
  def accept_loop(&block)
200
- Thread.current.backend.accept_loop(@io, TCPSocket, &block)
206
+ Polyphony.backend_accept_loop(@io, TCPSocket, &block)
201
207
  end
202
208
 
203
209
  alias_method :orig_close, :close
@@ -209,40 +215,43 @@ end
209
215
  class ::UNIXServer
210
216
  alias_method :orig_accept, :accept
211
217
  def accept
212
- Thread.current.backend.accept(self, UNIXSocket)
218
+ Polyphony.backend_accept(self, UNIXSocket)
213
219
  end
214
220
 
215
221
  def accept_loop(&block)
216
- Thread.current.backend.accept_loop(self, UNIXSocket, &block)
222
+ Polyphony.backend_accept_loop(self, UNIXSocket, &block)
217
223
  end
218
224
  end
219
225
 
220
226
  class ::UNIXSocket
221
227
  def recv(maxlen, flags = 0, outbuf = nil)
222
- Thread.current.backend.recv(self, outbuf || +'', maxlen)
228
+ Polyphony.backend_recv(self, outbuf || +'', maxlen)
223
229
  end
224
230
 
225
231
  def recv_loop(&block)
226
- Thread.current.backend.recv_loop(self, &block)
232
+ Polyphony.backend_recv_loop(self, &block)
227
233
  end
228
234
  alias_method :read_loop, :recv_loop
229
235
 
230
- def send(mesg, flags = 0)
231
- Thread.current.backend.send(self, mesg)
236
+ def feed_loop(receiver, method = :call, &block)
237
+ Polyphony.backend_recv_feed_loop(self, receiver, method, &block)
232
238
  end
233
239
 
234
- def write(str, *args)
235
- if args.empty?
236
- Thread.current.backend.send(self, str)
237
- else
238
- Thread.current.backend.send(self, str + args.join)
239
- end
240
+ def send(mesg, flags)
241
+ Polyphony.backend_send(self, mesg, flags)
242
+ end
243
+
244
+ def write(*args)
245
+ Polyphony.backend_sendv(self, args, 0)
246
+ end
247
+
248
+ def <<(mesg)
249
+ Polyphony.backend_send(self, mesg, 0)
240
250
  end
241
- alias_method :<<, :write
242
251
 
243
252
  def readpartial(maxlen, str = nil)
244
253
  @read_buffer ||= +''
245
- result = Thread.current.backend.recv(self, @read_buffer, maxlen)
254
+ result = Polyphony.backend_recv(self, @read_buffer, maxlen)
246
255
  raise EOFError unless result
247
256
 
248
257
  if str
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.50.0'
4
+ VERSION = '0.53.1'
5
5
  end
data/polyphony.gemspec CHANGED
@@ -22,12 +22,13 @@ Gem::Specification.new do |s|
22
22
  s.required_ruby_version = '>= 2.6'
23
23
 
24
24
  s.add_development_dependency 'rake-compiler', '1.1.1'
25
- s.add_development_dependency 'minitest', '5.13.0'
25
+ s.add_development_dependency 'minitest', '5.14.4'
26
26
  s.add_development_dependency 'minitest-reporters', '1.4.2'
27
27
  s.add_development_dependency 'simplecov', '0.17.1'
28
28
  s.add_development_dependency 'rubocop', '0.85.1'
29
29
  s.add_development_dependency 'pry', '0.13.1'
30
30
 
31
+ s.add_development_dependency 'msgpack', '1.4.2'
31
32
  s.add_development_dependency 'pg', '1.1.4'
32
33
  s.add_development_dependency 'redis', '4.1.0'
33
34
  s.add_development_dependency 'hiredis', '0.6.3'
@@ -37,8 +38,8 @@ Gem::Specification.new do |s|
37
38
  s.add_development_dependency 'sequel', '5.34.0'
38
39
  s.add_development_dependency 'httparty', '0.17.1'
39
40
 
40
- s.add_development_dependency 'jekyll', '~>3.8.6'
41
- s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
42
- s.add_development_dependency 'jekyll-seo-tag', '~>2.6.1'
43
- s.add_development_dependency 'just-the-docs', '~>0.3.0'
41
+ # s.add_development_dependency 'jekyll', '~>3.8.6'
42
+ # s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
43
+ # s.add_development_dependency 'jekyll-seo-tag', '~>2.6.1'
44
+ # s.add_development_dependency 'just-the-docs', '~>0.3.0'
44
45
  end
data/test/helper.rb CHANGED
@@ -52,7 +52,7 @@ class MiniTest::Test
52
52
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
53
53
  Thread.current.backend.finalize
54
54
  Thread.current.backend = Polyphony::Backend.new
55
- sleep 0
55
+ sleep 0.001
56
56
  end
57
57
 
58
58
  def teardown
data/test/stress.rb CHANGED
@@ -8,6 +8,8 @@ def run_test(count)
8
8
  puts "#{count}: running tests..."
9
9
  # sleep 1
10
10
  system(TEST_CMD)
11
+ puts
12
+
11
13
  return if $?.exitstatus == 0
12
14
 
13
15
  puts "Failure after #{count} tests"
data/test/test_backend.rb CHANGED
@@ -125,12 +125,15 @@ class BackendTest < MiniTest::Test
125
125
  assert_equal [:ready, 'foo', 'bar'], buf
126
126
  end
127
127
 
128
- def test_accept_loop
129
- server = TCPServer.new('127.0.0.1', 1234)
128
+ Net = Polyphony::Net
129
+
130
+ def test_accept
131
+ server = Net.listening_socket_from_options('127.0.0.1', 1234, reuse_addr: true)
130
132
 
131
133
  clients = []
132
- server_fiber = spin do
133
- @backend.accept_loop(server, TCPSocket) { |c| clients << c }
134
+ server_fiber = spin_loop do
135
+ c = @backend.accept(server, TCPSocket)
136
+ clients << c
134
137
  end
135
138
 
136
139
  c1 = TCPSocket.new('127.0.0.1', 1234)
@@ -146,7 +149,32 @@ class BackendTest < MiniTest::Test
146
149
  ensure
147
150
  c1&.close
148
151
  c2&.close
149
- server_fiber.stop
152
+ server_fiber&.stop
153
+ snooze
154
+ server&.close
155
+ end
156
+
157
+ def test_accept_loop
158
+ server = Net.listening_socket_from_options('127.0.0.1', 1235, reuse_addr: true)
159
+
160
+ clients = []
161
+ server_fiber = spin do
162
+ @backend.accept_loop(server, TCPSocket) { |c| clients << c }
163
+ end
164
+
165
+ c1 = TCPSocket.new('127.0.0.1', 1235)
166
+ sleep 0.01
167
+
168
+ assert_equal 1, clients.size
169
+
170
+ c2 = TCPSocket.new('127.0.0.1', 1235)
171
+ sleep 0.01
172
+
173
+ assert_equal 2, clients.size
174
+ ensure
175
+ c1&.close
176
+ c2&.close
177
+ server_fiber&.stop
150
178
  snooze
151
179
  server&.close
152
180
  end
@@ -209,4 +237,123 @@ class BackendTest < MiniTest::Test
209
237
  end
210
238
  assert_equal [1], buffer
211
239
  end
240
+
241
+ def test_splice
242
+ i1, o1 = IO.pipe
243
+ i2, o2 = IO.pipe
244
+ len = nil
245
+
246
+ spin {
247
+ len = o2.splice(i1, 1000)
248
+ o2.close
249
+ }
250
+
251
+ o1.write('foobar')
252
+ result = i2.read
253
+
254
+ assert_equal 'foobar', result
255
+ assert_equal 6, len
256
+ end
257
+
258
+ def test_splice_to_eof
259
+ i1, o1 = IO.pipe
260
+ i2, o2 = IO.pipe
261
+ len = nil
262
+
263
+ f = spin {
264
+ len = o2.splice_to_eof(i1, 1000)
265
+ o2.close
266
+ }
267
+
268
+ o1.write('foo')
269
+ result = i2.readpartial(1000)
270
+ assert_equal 'foo', result
271
+
272
+ o1.write('bar')
273
+ result = i2.readpartial(1000)
274
+ assert_equal 'bar', result
275
+ o1.close
276
+ f.await
277
+ assert_equal 6, len
278
+ ensure
279
+ if f.alive?
280
+ f.interrupt
281
+ f.await
282
+ end
283
+ end
284
+ end
285
+
286
+ class BackendChainTest < MiniTest::Test
287
+ def setup
288
+ super
289
+ @prev_backend = Thread.current.backend
290
+ @backend = Polyphony::Backend.new
291
+ Thread.current.backend = @backend
292
+ end
293
+
294
+ def teardown
295
+ @backend.finalize
296
+ Thread.current.backend = @prev_backend
297
+ end
298
+
299
+ def test_simple_write_chain
300
+ i, o = IO.pipe
301
+
302
+ result = Thread.backend.chain(
303
+ [:write, o, 'hello'],
304
+ [:write, o, ' world']
305
+ )
306
+
307
+ assert_equal 6, result
308
+ o.close
309
+ assert_equal 'hello world', i.read
310
+ end
311
+
312
+ def chunk_header(len)
313
+ "Content-Length: #{len}\r\n\r\n"
314
+ end
315
+
316
+ def serve_io(from, to)
317
+ i, o = IO.pipe
318
+ backend = Thread.current.backend
319
+ while true
320
+ len = o.splice(from, 8192)
321
+ break if len == 0
322
+
323
+ backend.chain(
324
+ [:write, to, chunk_header(len)],
325
+ [:splice, i, to, len]
326
+ )
327
+ end
328
+ to.close
329
+ end
330
+
331
+ def test_chain_with_splice
332
+ from_r, from_w = IO.pipe
333
+ to_r, to_w = IO.pipe
334
+
335
+ result = nil
336
+ f = spin { serve_io(from_r, to_w) }
337
+
338
+ from_w << 'Hello world!'
339
+ from_w.close
340
+
341
+ assert_equal "Content-Length: 12\r\n\r\nHello world!", to_r.read
342
+ end
343
+
344
+ def test_invalid_op
345
+ i, o = IO.pipe
346
+
347
+ assert_raises(RuntimeError) {
348
+ Thread.backend.chain(
349
+ [:read, o]
350
+ )
351
+ }
352
+
353
+ assert_raises(TypeError) {
354
+ Thread.backend.chain(
355
+ [:write, o]
356
+ )
357
+ }
358
+ end
212
359
  end