async-io 1.22.0 → 1.23.0

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