async-io 1.22.0 → 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 368fe5eb02962730d8f47e3ab180913aa922085eb554cc2e430b8c15113e8f33
4
- data.tar.gz: '09c5660537d3a7ce792bdf89674e79d0e40391346788c27808d14edbde1dfb13'
3
+ metadata.gz: b15a84835cd91c8d303fb774bcfab585180115153fab3006f0e2e179d9d06a91
4
+ data.tar.gz: 89d3f2c2c676234ce38c494d582502a78b7ff4f2a19a9eaa926dcdf95c45d770
5
5
  SHA512:
6
- metadata.gz: 27929ee0118185c0dadb8216ce88f5d4296633481215d0c3865396b56e5c9d73f63bd8b6d6f01db5e641a3eb6e78209746b36bf0e91b17cb9e7d8550819d8a37
7
- data.tar.gz: 35c33f0d712a94d7468c69aaa178139e756a3fcb1e56daf78671a2f347a790b141172ea6a3d993d4640da017d6f12624e1f10124225d0c4a877d2b39351b0b6f
6
+ metadata.gz: 42716527198f1cc5228b6c714cd43dd7ec8174aeab1a899db158e6e44c13067e8cc2c705048a0c00384334b19c5b9fe56e0c220cc676a1a190f8e0c6c9034a8b
7
+ data.tar.gz: '01831255a14a052f6a2fc04973d6a64002edbb31885ea8683ef0c84f104a4a74abd3a06ccc75f28115150bd6892456a9733af16e66280685ebe63dfedae83b26'
@@ -0,0 +1,32 @@
1
+ # wat.rb
2
+ require 'async'
3
+ require_relative '../../lib/async/io'
4
+ require 'digest/sha1'
5
+ require 'securerandom'
6
+
7
+ Async.run do |task|
8
+ r, w = IO.pipe.map { |io| Async::IO.try_convert(io) }
9
+
10
+ task.async do |subtask|
11
+ s = Digest::SHA1.new
12
+ l = 0
13
+ 100.times do
14
+ bytes = SecureRandom.bytes(4000)
15
+ s << bytes
16
+ w << bytes
17
+ l += bytes.bytesize
18
+ end
19
+ w.close
20
+ p [:write, l, s.hexdigest]
21
+ end
22
+
23
+ task.async do |subtask|
24
+ s = Digest::SHA1.new
25
+ l = 0
26
+ while b = r.read(4096)
27
+ s << b
28
+ l += b.bytesize
29
+ end
30
+ p [:read, l, s.hexdigest]
31
+ end
32
+ end
@@ -23,6 +23,10 @@ require 'forwardable'
23
23
 
24
24
  module Async
25
25
  module IO
26
+ # The default block size for IO buffers.
27
+ # BLOCK_SIZE = ENV.fetch('BLOCK_SIZE', 1024*16).to_i
28
+ BLOCK_SIZE = 1024*8
29
+
26
30
  # Convert a Ruby ::IO object to a wrapped instance:
27
31
  def self.try_convert(io, &block)
28
32
  if wrapper_class = Generic::WRAPPERS[io.class]
@@ -42,17 +46,20 @@ module Async
42
46
  # @!macro [attach] wrap_blocking_method
43
47
  # @method $1
44
48
  # Invokes `$2` on the underlying {io}. If the operation would block, the current task is paused until the operation can succeed, at which point it's resumed and the operation is completed.
45
- def wrap_blocking_method(new_name, method_name, invert: true)
46
- define_method(new_name) do |*args|
47
- async_send(method_name, *args)
49
+ def wrap_blocking_method(new_name, method_name, invert: true, &block)
50
+ if block_given?
51
+ define_method(new_name, &block)
52
+ else
53
+ define_method(new_name) do |*args|
54
+ async_send(method_name, *args)
55
+ end
48
56
  end
49
57
 
50
58
  if invert
51
- # We define the original _nonblock method to call the async variant. We ignore options.
52
- # define_method(method_name) do |*args, **options|
53
- # self.__send__(new_name, *args)
54
- # end
55
- def_delegators :@io, method_name
59
+ # We wrap the original _nonblock method, ignoring options.
60
+ define_method(method_name) do |*args, exception: false|
61
+ async_send(method_name, *args)
62
+ end
56
63
  end
57
64
  end
58
65
 
@@ -85,17 +92,77 @@ module Async
85
92
 
86
93
  wraps ::IO, :external_encoding, :internal_encoding, :autoclose?, :autoclose=, :pid, :stat, :binmode, :flush, :set_encoding, :to_io, :to_i, :reopen, :fileno, :fsync, :fdatasync, :sync, :sync=, :tell, :seek, :rewind, :pos, :pos=, :eof, :eof?, :close_on_exec?, :close_on_exec=, :closed?, :close_read, :close_write, :isatty, :tty?, :binmode?, :sysseek, :advise, :ioctl, :fcntl, :nread, :ready?, :pread, :pwrite, :pathconf
87
94
 
95
+ # Read the specified number of bytes from the input stream. This is fast path.
88
96
  # @example
89
- # data = io.read(512)
90
- wrap_blocking_method :read, :read_nonblock
91
- alias sysread read
92
- alias readpartial read
97
+ # data = io.sysread(512)
98
+ wrap_blocking_method :sysread, :read_nonblock
99
+
100
+ # Read `length` bytes of data from the underlying I/O. If length is unspecified, read everything.
101
+ def read(length = nil, buffer = nil)
102
+ if buffer
103
+ buffer.clear
104
+ else
105
+ buffer = String.new
106
+ end
107
+
108
+ if length
109
+ return "" if length <= 0
110
+
111
+ # Fast path:
112
+ if buffer = self.sysread(length, buffer)
113
+
114
+ # Slow path:
115
+ while buffer.bytesize < length
116
+ # Slow path:
117
+ if chunk = self.sysread(length - buffer.bytesize)
118
+ buffer << chunk
119
+ else
120
+ break
121
+ end
122
+ end
123
+
124
+ return buffer
125
+ else
126
+ return nil
127
+ end
128
+ else
129
+ buffer = self.sysread(BLOCK_SIZE, buffer)
130
+
131
+ while chunk = self.sysread(BLOCK_SIZE)
132
+ buffer << chunk
133
+ end
134
+
135
+ return buffer
136
+ end
137
+ end
93
138
 
139
+ # Write entire buffer to output stream. This is fast path.
94
140
  # @example
95
- # io.write("Hello World")
96
- wrap_blocking_method :write, :write_nonblock
97
- alias syswrite write
98
- alias << write
141
+ # io.syswrite("Hello World")
142
+ wrap_blocking_method :syswrite, :write_nonblock
143
+
144
+ alias readpartial read_nonblock
145
+
146
+ def write(buffer)
147
+ # Fast path:
148
+ written = self.syswrite(buffer)
149
+ remaining = buffer.bytesize - written
150
+
151
+ while remaining > 0
152
+ # Slow path:
153
+ length = self.syswrite(buffer.byteslice(written, remaining))
154
+
155
+ remaining -= length
156
+ written += length
157
+ end
158
+
159
+ return written
160
+ end
161
+
162
+ def << buffer
163
+ write(buffer)
164
+ return self
165
+ end
99
166
 
100
167
  def dup
101
168
  super.tap do |copy|
@@ -58,7 +58,7 @@ module Async
58
58
  # On Darwin, sometimes occurs when the connection is not yet fully formed. Empirically, TCP_NODELAY is enabled despite this result.
59
59
  rescue Errno::EOPNOTSUPP
60
60
  # Some platforms may simply not support the operation.
61
- Async.logger.warn(self) {"Unable to set sync=#{value}!"}
61
+ # Async.logger.warn(self) {"Unable to set sync=#{value}!"}
62
62
  end
63
63
 
64
64
  def sync
@@ -18,7 +18,7 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require_relative 'endpoint'
21
+ require_relative 'host_endpoint'
22
22
  require_relative 'ssl_socket'
23
23
 
24
24
  module Async
@@ -33,9 +33,6 @@ module Async
33
33
  wrap_blocking_method :accept, :accept_nonblock
34
34
  wrap_blocking_method :connect, :connect_nonblock
35
35
 
36
- alias syswrite write
37
- alias sysread read
38
-
39
36
  def self.connect(socket, context, hostname = nil, &block)
40
37
  client = self.new(socket, context)
41
38
 
@@ -62,14 +59,6 @@ module Async
62
59
  end
63
60
  end
64
61
 
65
- def local_address
66
- @io.to_io.local_address
67
- end
68
-
69
- def remote_address
70
- @io.to_io.remote_address
71
- end
72
-
73
62
  include Peer
74
63
 
75
64
  def initialize(socket, context)
@@ -90,6 +79,27 @@ module Async
90
79
  super(io, socket.reactor)
91
80
  end
92
81
  end
82
+
83
+ def local_address
84
+ @io.to_io.local_address
85
+ end
86
+
87
+ def remote_address
88
+ @io.to_io.remote_address
89
+ end
90
+
91
+ def close_write
92
+ self.shutdown(Socket::SHUT_WR)
93
+ end
94
+
95
+ def close_read
96
+ self.shutdown(Socket::SHUT_RD)
97
+ end
98
+
99
+ def shutdown(how)
100
+ @io.flush
101
+ @io.to_io.shutdown(how)
102
+ end
93
103
  end
94
104
 
95
105
  # We reimplement this from scratch because the native implementation doesn't expose the underlying server/context that we need to implement non-blocking accept.
@@ -24,9 +24,7 @@ require_relative 'generic'
24
24
  module Async
25
25
  module IO
26
26
  class Stream
27
- # The default block size for IO buffers.
28
- # BLOCK_SIZE = ENV.fetch('BLOCK_SIZE', 1024*16).to_i
29
- BLOCK_SIZE = 1024*8
27
+ BLOCK_SIZE = IO::BLOCK_SIZE
30
28
 
31
29
  def initialize(io, block_size: BLOCK_SIZE, sync: true)
32
30
  @io = io
@@ -72,13 +70,29 @@ module Async
72
70
  def read_partial(size = nil)
73
71
  return '' if size == 0
74
72
 
75
- if @read_buffer.empty? and !@eof
76
- fill_read_buffer
73
+ unless @eof
74
+ if size and @read_buffer.bytesize < size
75
+ fill_read_buffer(size > @block_size ? size : @block_size)
76
+ elsif @read_buffer.empty?
77
+ fill_read_buffer
78
+ end
77
79
  end
78
80
 
79
81
  return consume_read_buffer(size)
80
82
  end
81
83
 
84
+ def read_exactly(size, exception: EOFError)
85
+ if buffer = read(size)
86
+ if buffer.bytesize != size
87
+ raise exception, "could not read enough data"
88
+ end
89
+
90
+ return buffer
91
+ end
92
+
93
+ raise exception, "encountered eof while reading data"
94
+ end
95
+
82
96
  alias readpartial read_partial
83
97
 
84
98
  # Efficiently read data from the stream until encountering pattern.
@@ -115,12 +129,12 @@ module Async
115
129
  # @return the number of bytes appended to the buffer.
116
130
  def write(string)
117
131
  if @write_buffer.empty? and string.bytesize >= @block_size
118
- syswrite(string)
132
+ @io.write(string)
119
133
  else
120
134
  @write_buffer << string
121
135
 
122
136
  if @write_buffer.size >= @block_size
123
- syswrite(@write_buffer)
137
+ @io.write(@write_buffer)
124
138
  @write_buffer.clear
125
139
  end
126
140
  end
@@ -138,7 +152,7 @@ module Async
138
152
  # Flushes buffered data to the stream.
139
153
  def flush
140
154
  unless @write_buffer.empty?
141
- syswrite(@write_buffer)
155
+ @io.write(@write_buffer)
142
156
  @write_buffer.clear
143
157
  end
144
158
  end
@@ -207,9 +221,9 @@ module Async
207
221
 
208
222
  # Fills the buffer from the underlying stream.
209
223
  def fill_read_buffer(size = @block_size)
210
- if @read_buffer.empty? and @io.read(size, @read_buffer)
224
+ if @read_buffer.empty? and @io.read_nonblock(size, @read_buffer, exception: false)
211
225
  return true
212
- elsif chunk = @io.read(size, @input_buffer)
226
+ elsif chunk = @io.read_nonblock(size, @input_buffer, exception: false)
213
227
  @read_buffer << chunk
214
228
  return true
215
229
  else
@@ -245,28 +259,6 @@ module Async
245
259
 
246
260
  return result
247
261
  end
248
-
249
- # Write a buffer to the underlying stream.
250
- # @param buffer [String] The string to write, any encoding is okay.
251
- def syswrite(buffer)
252
- remaining = buffer.bytesize
253
-
254
- # Fast path:
255
- written = @io.write(buffer)
256
- return if written == remaining
257
-
258
- # Slow path:
259
- remaining -= written
260
-
261
- while remaining > 0
262
- wrote = @io.write(buffer.byteslice(written, remaining))
263
-
264
- remaining -= wrote
265
- written += wrote
266
- end
267
-
268
- return written
269
- end
270
262
  end
271
263
  end
272
264
  end
@@ -28,33 +28,7 @@ module Async
28
28
  class TCPSocket < IPSocket
29
29
  wraps ::TCPSocket
30
30
 
31
- class StreamWrapper
32
- def initialize(io)
33
- @io = io
34
- end
35
-
36
- def sync= value
37
- @io.sync = value
38
- end
39
-
40
- def close
41
- @io.close
42
- end
43
-
44
- def read(*args)
45
- @io.sysread(*args)
46
- end
47
-
48
- def write(*args)
49
- @io.syswrite(*args)
50
- end
51
-
52
- def flush
53
- @io.flush
54
- end
55
- end
56
-
57
- def initialize(remote_host, remote_port = nil, local_host = nil, local_port = 0)
31
+ def initialize(remote_host, remote_port = nil, local_host = nil, local_port = nil)
58
32
  if remote_host.is_a? ::TCPSocket
59
33
  super(remote_host)
60
34
  else
@@ -73,33 +47,33 @@ module Async
73
47
  # super(::TCPSocket.new(remote_host, remote_port, local_host, local_port))
74
48
  end
75
49
 
76
- @buffer = Stream.new(StreamWrapper.new(self))
50
+ @stream = Stream.new(self)
77
51
  end
78
52
 
79
53
  class << self
80
54
  alias open new
81
55
  end
82
56
 
83
- include Peer
57
+ def close
58
+ @stream.flush
59
+ super
60
+ end
84
61
 
85
- attr :buffer
62
+ include Peer
86
63
 
87
- def_delegators :@buffer, :gets, :puts, :flush
64
+ attr :stream
88
65
 
89
- def write(*)
90
- @buffer.flush
91
-
92
- super
93
- end
66
+ # The way this buffering works is pretty atrocious.
67
+ def_delegators :@stream, :gets, :puts
94
68
 
95
- def read(size, outbuf = nil)
96
- buffer = @buffer.read_partial(size)
69
+ def sysread(size, buffer = nil)
70
+ data = @stream.read_partial(size)
97
71
 
98
- if outbuf
99
- outbuf.replace(buffer)
72
+ if buffer
73
+ buffer.replace(data)
100
74
  end
101
75
 
102
- return buffer
76
+ return data
103
77
  end
104
78
  end
105
79
 
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module IO
23
- VERSION = "1.22.0"
23
+ VERSION = "1.23.0"
24
24
  end
25
25
  end
@@ -44,8 +44,9 @@ RSpec.describe "echo client/server" do
44
44
  Async do |task|
45
45
  Async::IO::Socket.connect(server_address) do |peer|
46
46
  result = peer.write(data)
47
+ peer.close_write
47
48
 
48
- message = peer.read(512)
49
+ message = peer.read(data.bytesize)
49
50
 
50
51
  responses << message
51
52
  end
@@ -22,3 +22,31 @@ RSpec.shared_examples Async::IO::Generic do |ignore_methods|
22
22
  # end
23
23
  # end
24
24
  end
25
+
26
+ RSpec.shared_examples Async::IO do
27
+ let(:data) {"Hello World!"}
28
+
29
+ it "should read data" do
30
+ io.write(data)
31
+ expect(subject.read(data.bytesize)).to be == data
32
+ end
33
+
34
+ it "should read less than available data" do
35
+ io.write(data)
36
+ expect(subject.read(1)).to be == data[0]
37
+ end
38
+
39
+ it "should read all available data" do
40
+ io.write(data)
41
+ io.close_write
42
+
43
+ expect(subject.read(data.bytesize * 2)).to be == data
44
+ end
45
+
46
+ it "should read all available data" do
47
+ io.write(data)
48
+ io.close_write
49
+
50
+ expect(subject.read).to be == data
51
+ end
52
+ end
@@ -43,17 +43,16 @@ RSpec.describe Async::IO::Generic do
43
43
 
44
44
  output_task = reactor.async do
45
45
  received = input.read(1024)
46
+ input.close
46
47
  end
47
48
 
48
49
  reactor.async do
49
50
  output.write(message)
51
+ output.close
50
52
  end
51
53
 
52
54
  output_task.wait
53
55
  expect(received).to be == message
54
-
55
- input.close
56
- output.close
57
56
  end
58
57
 
59
58
  describe '#wait' do
@@ -47,6 +47,7 @@ RSpec.describe Async::IO::Socket do
47
47
  reactor.async do
48
48
  Async::IO::Socket.connect(server_address) do |client|
49
49
  client.write(data)
50
+ client.close_write
50
51
 
51
52
  expect(client.read(512)).to be == data
52
53
  end
@@ -59,6 +60,7 @@ RSpec.describe Async::IO::Socket do
59
60
  reactor.async do |task|
60
61
  Async::IO::Socket.connect(server_address, local_address: local_address) do |client|
61
62
  client.write(data)
63
+ client.close_write
62
64
 
63
65
  expect(client.read(512)).to be == data
64
66
  end
@@ -69,6 +71,7 @@ RSpec.describe Async::IO::Socket do
69
71
  reactor.async do |task|
70
72
  Async::IO::Socket.connect(server_address) do |client|
71
73
  client.write(data)
74
+ client.close_write
72
75
 
73
76
  expect(client.read(512)).to be == data
74
77
  end
@@ -84,6 +87,7 @@ RSpec.describe Async::IO::Socket do
84
87
 
85
88
  reactor.async do
86
89
  socket.write(data)
90
+ socket.close_write
87
91
 
88
92
  expect(socket.read(512)).to be == data
89
93
 
@@ -19,6 +19,7 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require 'async/io/socket'
22
+ require 'async/io/address'
22
23
 
23
24
  require_relative 'generic_examples'
24
25
 
@@ -124,6 +125,21 @@ RSpec.describe Async::IO::Socket do
124
125
  s2.close
125
126
  end
126
127
  end
128
+
129
+ context '.pipe' do
130
+ let(:sockets) do
131
+ @sockets = described_class.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
132
+ end
133
+
134
+ after do
135
+ @sockets&.each(&:close)
136
+ end
137
+
138
+ let(:io) {sockets.first}
139
+ subject {sockets.last}
140
+
141
+ it_should_behave_like Async::IO
142
+ end
127
143
  end
128
144
 
129
145
  RSpec.describe Async::IO::IPSocket do
@@ -52,6 +52,7 @@ RSpec.describe Async::IO::SSLServer do
52
52
  reactor.async do
53
53
  client_endpoint.connect do |client|
54
54
  client.write(data)
55
+ client.close_write
55
56
 
56
57
  expect(client.read(512)).to be == data
57
58
  end
@@ -91,6 +92,7 @@ RSpec.describe Async::IO::SSLServer do
91
92
  reactor.async do
92
93
  valid_client_endpoint.connect do |client|
93
94
  client.write(data)
95
+ client.close_write
94
96
 
95
97
  expect(client.read(512)).to be == data
96
98
  end
@@ -18,7 +18,7 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'async/io/ssl_socket'
21
+ require 'async/io/ssl_endpoint'
22
22
 
23
23
  require 'async/rspec/ssl'
24
24
  require_relative 'generic_examples'
@@ -69,6 +69,7 @@ RSpec.describe Async::IO::SSLSocket do
69
69
  expect(client.timeout).to be == 10
70
70
 
71
71
  client.write(data)
72
+ client.close_write
72
73
 
73
74
  expect(client.read(512)).to be == data
74
75
  end
@@ -0,0 +1,28 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/rspec/buffer'
22
+ require 'async/io/stream'
23
+
24
+ RSpec.shared_context Async::IO::Stream do
25
+ include_context Async::RSpec::Buffer
26
+ subject {described_class.new(buffer)}
27
+ let(:io) {subject.io}
28
+ end
@@ -18,203 +18,251 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'async/io/stream'
22
- require 'async/rspec/buffer'
21
+ require 'async/io/socket'
22
+
23
+ require_relative 'generic_examples'
24
+ require_relative 'stream_context'
23
25
 
24
26
  RSpec.describe Async::IO::Stream do
25
- include_context Async::RSpec::Buffer
26
- include_context Async::RSpec::Memory
27
- include_context Async::RSpec::Reactor
28
-
29
- let!(:stream) {Async::IO::Stream.new(buffer)}
30
- let(:io) {stream.io}
27
+ # This constant is part of the public interface, but was renamed to `Async::IO::BLOCK_SIZE`.
28
+ describe "::BLOCK_SIZE" do
29
+ it "should exist and be reasonable" do
30
+ expect(Async::IO::Stream::BLOCK_SIZE).to be_between(1024, 1024*32)
31
+ end
32
+ end
31
33
 
32
- describe '#close_read' do
33
- let(:sockets) {@sockets = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)}
34
- let!(:stream) {Async::IO::Stream.new(sockets.last)}
35
- after(:each) {@sockets&.each(&:close)}
34
+ context "socket I/O" do
35
+ let(:sockets) do
36
+ @sockets = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
37
+ end
36
38
 
37
- it "can close the reading end of the stream" do
38
- stream.close_read
39
-
40
- expect do
41
- stream.read
42
- end.to raise_error(IOError, /not opened for reading/)
39
+ after do
40
+ @sockets&.each(&:close)
43
41
  end
44
42
 
45
- it "can close the writing end of the stream" do
46
- stream.close_write
43
+ let(:io) {sockets.first}
44
+ subject {described_class.new(sockets.last)}
45
+
46
+ it_should_behave_like Async::IO
47
+
48
+ describe '#close_read' do
49
+ let(:sockets) do
50
+ @sockets = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
51
+ end
47
52
 
48
- expect do
49
- stream.write("Oh no!")
50
- stream.flush
51
- end.to raise_error(IOError, /not opened for writing/)
52
- end
53
- end
54
-
55
- describe '#read' do
56
- it "should read everything" do
57
- io.write "Hello World"
58
- io.seek(0)
53
+ after do
54
+ @sockets&.each(&:close)
55
+ end
56
+
57
+ subject {described_class.new(sockets.last)}
59
58
 
60
- expect(io).to receive(:read).and_call_original.twice
59
+ it "can close the reading end of the stream" do
60
+ expect(subject.io).to receive(:close_read).and_call_original
61
+
62
+ subject.close_read
63
+
64
+ # Ruby <= 2.4 raises an exception even with exception: false
65
+ # expect(stream.read).to be_nil
66
+ end
61
67
 
62
- expect(stream.read).to be == "Hello World"
63
- expect(stream).to be_eof
68
+ it "can close the writing end of the stream" do
69
+ expect(subject.io).to receive(:close_write).and_call_original
70
+
71
+ subject.write("Oh yes!")
72
+ subject.close_write
73
+
74
+ expect do
75
+ subject.write("Oh no!")
76
+ subject.flush
77
+ end.to raise_error(IOError, /not opened for writing/)
78
+ end
64
79
  end
65
80
 
66
- it "should read only the amount requested" do
67
- io.write "Hello World"
68
- io.seek(0)
69
-
70
- expect(io).to receive(:read).and_call_original.twice
71
-
72
- expect(stream.read(4)).to be == "Hell"
73
- expect(stream).to_not be_eof
81
+ describe '#read_exactly' do
82
+ it "can read several bytes" do
83
+ io.write("hello\nworld\n")
84
+
85
+ expect(subject.read_exactly(4)).to be == 'hell'
86
+ end
74
87
 
75
- expect(stream.read(20)).to be == "o World"
76
- expect(stream).to be_eof
88
+ it "can raise exception if io is eof" do
89
+ io.close
90
+
91
+ expect do
92
+ subject.read_exactly(4)
93
+ end.to raise_error(EOFError)
94
+ end
77
95
  end
96
+ end
97
+
98
+ context "buffered I/O" do
99
+ include_context Async::IO::Stream
100
+ include_context Async::RSpec::Memory
101
+ include_context Async::RSpec::Reactor
78
102
 
79
- context "with large content" do
80
- it "allocates expected amount of bytes" do
81
- io.write("." * 16*1024)
103
+ describe '#read' do
104
+ it "should read everything" do
105
+ io.write "Hello World"
82
106
  io.seek(0)
83
107
 
84
- buffer = nil
108
+ expect(subject.io).to receive(:read_nonblock).and_call_original.twice
85
109
 
86
- expect do
87
- # The read buffer is already allocated, and it will be resized to fit the incoming data. It will be swapped with an empty buffer.
88
- buffer = stream.read(16*1024)
89
- end.to limit_allocations.of(String, count: 1, size: 0)
110
+ expect(subject.read).to be == "Hello World"
111
+ expect(subject).to be_eof
112
+ end
113
+
114
+ it "should read only the amount requested" do
115
+ io.write "Hello World"
116
+ io.seek(0)
90
117
 
91
- expect(buffer.size).to be == 16*1024
118
+ expect(subject.io).to receive(:read_nonblock).and_call_original.twice
92
119
 
93
- io.close
120
+ expect(subject.read_partial(4)).to be == "Hell"
121
+ expect(subject).to_not be_eof
122
+
123
+ expect(subject.read_partial(20)).to be == "o World"
124
+ expect(subject).to be_eof
125
+ end
126
+
127
+ context "with large content" do
128
+ it "allocates expected amount of bytes" do
129
+ io.write("." * 16*1024)
130
+ io.seek(0)
131
+
132
+ buffer = nil
133
+
134
+ expect do
135
+ # The read buffer is already allocated, and it will be resized to fit the incoming data. It will be swapped with an empty buffer.
136
+ buffer = subject.read(16*1024)
137
+ end.to limit_allocations.of(String, count: 1, size: 0)
138
+
139
+ expect(buffer.size).to be == 16*1024
140
+ end
94
141
  end
95
- end
96
- end
97
-
98
- describe '#read_until' do
99
- it "can read a line" do
100
- io.write("hello\nworld\n")
101
- io.seek(0)
102
-
103
- expect(stream.read_until("\n")).to be == 'hello'
104
- expect(stream.read_until("\n")).to be == 'world'
105
- expect(stream.read_until("\n")).to be_nil
106
142
  end
107
143
 
108
- context "with 1-byte block size" do
109
- let!(:stream) {Async::IO::Stream.new(buffer, block_size: 1)}
110
-
111
- it "can read a line with a multi-byte pattern" do
112
- io.write("hello\r\nworld\r\n")
144
+ describe '#read_until' do
145
+ it "can read a line" do
146
+ io.write("hello\nworld\n")
113
147
  io.seek(0)
114
148
 
115
- expect(stream.read_until("\r\n")).to be == 'hello'
116
- expect(stream.read_until("\r\n")).to be == 'world'
117
- expect(stream.read_until("\r\n")).to be_nil
149
+ expect(subject.read_until("\n")).to be == 'hello'
150
+ expect(subject.read_until("\n")).to be == 'world'
151
+ expect(subject.read_until("\n")).to be_nil
118
152
  end
119
- end
120
153
 
121
- context "with large content" do
122
- it "allocates expected amount of bytes" do
123
- expect do
124
- stream.read_until("b")
125
- end.to limit_allocations.of(String, size: 0, count: 1)
154
+ context "with 1-byte block size" do
155
+ subject! {Async::IO::Stream.new(buffer, block_size: 1)}
156
+
157
+ it "can read a line with a multi-byte pattern" do
158
+ io.write("hello\r\nworld\r\n")
159
+ io.seek(0)
160
+
161
+ expect(subject.read_until("\r\n")).to be == 'hello'
162
+ expect(subject.read_until("\r\n")).to be == 'world'
163
+ expect(subject.read_until("\r\n")).to be_nil
164
+ end
126
165
  end
127
- end
128
- end
129
-
130
- describe '#flush' do
131
- it "should not call write if write buffer is empty" do
132
- expect(io).to_not receive(:write)
133
166
 
134
- stream.flush
167
+ context "with large content" do
168
+ it "allocates expected amount of bytes" do
169
+ subject
170
+
171
+ expect do
172
+ subject.read_until("b")
173
+ end.to limit_allocations.of(String, size: 0, count: 1)
174
+ end
175
+ end
135
176
  end
136
177
 
137
- it "should flush underlying data when it exceeds block size" do
138
- expect(io).to receive(:write).and_call_original.once
139
-
140
- stream.block_size.times do
141
- stream.write("!")
178
+ describe '#flush' do
179
+ it "should not call write if write buffer is empty" do
180
+ expect(subject.io).to_not receive(:write)
181
+
182
+ subject.flush
142
183
  end
143
- end
144
- end
145
-
146
- describe '#read_partial' do
147
- before(:each) do
148
- io.write "Hello World!" * 1024
149
- io.seek(0)
150
- end
151
184
 
152
- it "should avoid calling read" do
153
- expect(io).to receive(:read).and_call_original.once
154
-
155
- expect(stream.read_partial(12)).to be == "Hello World!"
185
+ it "should flush underlying data when it exceeds block size" do
186
+ expect(subject.io).to receive(:write).and_call_original.once
187
+
188
+ subject.block_size.times do
189
+ subject.write("!")
190
+ end
191
+ end
156
192
  end
157
193
 
158
- context "with large content" do
159
- it "allocates only the amount required" do
160
- expect do
161
- stream.read(4*1024)
162
- end.to limit_allocations.of(String, count: 2, size: 4*1024+1)
194
+ describe '#read_partial' do
195
+ before(:each) do
196
+ io.write("Hello World!" * 1024)
197
+ io.seek(0)
163
198
  end
164
199
 
165
- it "allocates exact number of bytes being read" do
166
- expect do
167
- stream.read(16*1024)
168
- end.to limit_allocations.of(String, count: 1, size: 0)
200
+ it "should avoid calling read" do
201
+ expect(subject.io).to receive(:read_nonblock).and_call_original.once
202
+
203
+ expect(subject.read_partial(12)).to be == "Hello World!"
169
204
  end
170
205
 
171
- it "allocates expected amount of bytes" do
172
- buffer = nil
206
+ context "with large content" do
207
+ it "allocates only the amount required" do
208
+ expect do
209
+ subject.read(4*1024)
210
+ end.to limit_allocations.of(String, count: 2, size: 4*1024+1)
211
+ end
173
212
 
174
- expect do
175
- buffer = stream.read_partial
176
- end.to limit_allocations.of(String, count: 1)
213
+ it "allocates exact number of bytes being read" do
214
+ expect do
215
+ subject.read_partial(16*1024)
216
+ end.to limit_allocations.of(String, count: 1, size: 0)
217
+ end
177
218
 
178
- expect(buffer.size).to be == stream.block_size
219
+ it "allocates expected amount of bytes" do
220
+ buffer = nil
221
+
222
+ expect do
223
+ buffer = subject.read_partial
224
+ end.to limit_allocations.of(String, count: 1)
225
+
226
+ expect(buffer.size).to be == subject.block_size
227
+ end
179
228
  end
180
229
  end
181
- end
182
-
183
- describe '#write' do
184
- it "should read one line" do
185
- expect(io).to receive(:write).and_call_original.once
186
-
187
- stream.write "Hello World\n"
188
- stream.flush
189
-
190
- io.seek(0)
191
-
192
- expect(stream.read).to be == "Hello World\n"
230
+
231
+ describe '#write' do
232
+ it "should read one line" do
233
+ expect(subject.io).to receive(:write).and_call_original.once
234
+
235
+ subject.write "Hello World\n"
236
+ subject.flush
237
+
238
+ io.seek(0)
239
+ expect(subject.read).to be == "Hello World\n"
240
+ end
193
241
  end
194
- end
195
-
196
- describe '#eof' do
197
- it "should terminate stream" do
198
- expect do
199
- stream.eof!
200
- end.to raise_exception(EOFError)
201
-
202
- expect(stream).to be_eof
242
+
243
+ describe '#eof' do
244
+ it "should terminate subject" do
245
+ expect do
246
+ subject.eof!
247
+ end.to raise_exception(EOFError)
248
+
249
+ expect(subject).to be_eof
250
+ end
203
251
  end
204
- end
205
-
206
- describe '#close' do
207
- it 'can be closed even if underlying io is closed' do
208
- io.close
209
-
210
- expect(stream.io).to be_closed
211
-
212
- # Put some data in the write buffer
213
- stream.write "."
214
-
215
- expect do
216
- stream.close
217
- end.to_not raise_exception
252
+
253
+ describe '#close' do
254
+ it 'can be closed even if underlying io is closed' do
255
+ io.close
256
+
257
+ expect(subject.io).to be_closed
258
+
259
+ # Put some data in the write buffer
260
+ subject.write "."
261
+
262
+ expect do
263
+ subject.close
264
+ end.to_not raise_exception
265
+ end
218
266
  end
219
267
  end
220
268
  end
@@ -22,7 +22,7 @@ require 'async/io/tcp_socket'
22
22
 
23
23
  require_relative 'generic_examples'
24
24
 
25
- RSpec.describe Async::IO::TCPSocket do
25
+ RSpec.describe Async::IO::TCPSocket, timeout: 1 do
26
26
  include_context Async::RSpec::Reactor
27
27
 
28
28
  it_should_behave_like Async::IO::Generic
@@ -44,6 +44,7 @@ RSpec.describe Async::IO::TCPSocket do
44
44
 
45
45
  data = peer.gets
46
46
  peer.puts(data)
47
+ peer.flush
47
48
 
48
49
  peer.close
49
50
  server.close
@@ -52,15 +53,16 @@ RSpec.describe Async::IO::TCPSocket do
52
53
 
53
54
  let(:client) {Async::IO::TCPSocket.new("localhost", 6788)}
54
55
 
55
- it "can read into outbuf" do
56
+ it "can read into output buffer" do
56
57
  client.puts("Hello World")
58
+ client.flush
57
59
 
58
- outbuf = String.new
60
+ buffer = String.new
59
61
  # 20 is bigger than echo response...
60
- data = client.read(20, outbuf)
62
+ data = client.read(20, buffer)
61
63
 
62
- expect(outbuf).to_not be_empty
63
- expect(outbuf).to be == data
64
+ expect(buffer).to_not be_empty
65
+ expect(buffer).to be == data
64
66
 
65
67
  client.close
66
68
  server_task.wait
@@ -69,6 +71,8 @@ RSpec.describe Async::IO::TCPSocket do
69
71
  it "should start server and send data" do
70
72
  # Accept a single incoming connection and then finish.
71
73
  client.puts(data)
74
+ client.flush
75
+
72
76
  expect(client.gets).to be == data
73
77
 
74
78
  client.close
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-io
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.22.0
4
+ version: 1.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-30 00:00:00.000000000 Z
11
+ date: 2019-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -130,6 +130,7 @@ files:
130
130
  - examples/chat/client.rb
131
131
  - examples/chat/server.rb
132
132
  - examples/issues/broken_ssl.rb
133
+ - examples/issues/pipes.rb
133
134
  - examples/millions/client.rb
134
135
  - examples/millions/server.rb
135
136
  - examples/udp/client.rb
@@ -176,6 +177,7 @@ files:
176
177
  - spec/async/io/ssl_server_spec.rb
177
178
  - spec/async/io/ssl_socket_spec.rb
178
179
  - spec/async/io/standard_spec.rb
180
+ - spec/async/io/stream_context.rb
179
181
  - spec/async/io/stream_spec.rb
180
182
  - spec/async/io/tcp_socket_spec.rb
181
183
  - spec/async/io/trap_spec.rb
@@ -204,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
206
  - !ruby/object:Gem::Version
205
207
  version: '0'
206
208
  requirements: []
207
- rubygems_version: 3.0.3
209
+ rubygems_version: 3.0.2
208
210
  signing_key:
209
211
  specification_version: 4
210
212
  summary: Provides support for asynchonous TCP, UDP, UNIX and SSL sockets.
@@ -226,6 +228,7 @@ test_files:
226
228
  - spec/async/io/ssl_server_spec.rb
227
229
  - spec/async/io/ssl_socket_spec.rb
228
230
  - spec/async/io/standard_spec.rb
231
+ - spec/async/io/stream_context.rb
229
232
  - spec/async/io/stream_spec.rb
230
233
  - spec/async/io/tcp_socket_spec.rb
231
234
  - spec/async/io/trap_spec.rb