io_unblock 0.1.1 → 0.1.2

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.
@@ -1,10 +1,10 @@
1
1
  require 'thread'
2
- require "io_unblock/version"
3
-
4
- module IoUnblock
5
- class IoUnblockError < StandardError; end
6
- end
7
-
2
+ require 'socket'
3
+ require 'forwardable'
4
+ require 'io_unblock/version'
5
+ require 'io_unblock/errors'
8
6
  require 'io_unblock/delegation'
9
7
  require 'io_unblock/buffer'
10
8
  require 'io_unblock/stream'
9
+ require 'io_unblock/tcp_socket'
10
+
@@ -9,16 +9,16 @@ module IoUnblock
9
9
  @mutex = Mutex.new
10
10
  end
11
11
 
12
- def push bytes, cb
13
- synched { @buffer.push [bytes, cb] }
12
+ def push bytes, cb, cb_args
13
+ synched { @buffer.push [bytes, cb, cb_args] }
14
14
  end
15
15
 
16
16
  def pop
17
17
  synched { @buffer.pop }
18
18
  end
19
19
 
20
- def unshift bytes, cb
21
- synched { @buffer.unshift [bytes, cb] }
20
+ def unshift bytes, cb, cb_args
21
+ synched { @buffer.unshift [bytes, cb, cb_args] }
22
22
  end
23
23
 
24
24
  def shift
@@ -0,0 +1,4 @@
1
+ module IoUnblock
2
+ class IoUnblockError < StandardError; end
3
+ end
4
+
@@ -54,35 +54,55 @@ module IoUnblock
54
54
  end
55
55
 
56
56
  def stop
57
- @s_mutex.synchronize do
58
- if @running
59
- @running = false
60
- @processor.join
61
- end
57
+ if @processor == Thread.current
58
+ stop_inside
59
+ else
60
+ stop_outside
62
61
  end
63
62
  self
64
63
  end
65
64
 
66
65
  # The callback triggered here will be invoked only when all bytes
67
66
  # have been written.
68
- def write bytes, &cb
69
- @w_buff.push bytes, cb
67
+ def write bytes, *cb_args, &cb
68
+ @w_buff.push bytes, cb, cb_args
70
69
  self
71
70
  end
71
+
72
+ def alive?
73
+ @processor && @processor.alive?
74
+ end
72
75
 
73
76
  private
74
- def trigger_callbacks named, *args, &other
75
- other && other.call(*args)
76
- @callbacks.key?(named) && @callbacks[named].call(*args)
77
+ def stop_inside
78
+ @running = false
79
+ end
80
+
81
+ def stop_outside
82
+ @s_mutex.synchronize do
83
+ if @running
84
+ @running = false
85
+ @processor.join
86
+ end
87
+ end
88
+ end
89
+
90
+ def guard_callback named, cb, *args
91
+ cb && cb.call(*args)
77
92
  rescue Exception => ex
78
93
  if named == :callback_failed
79
94
  warn "Exception raised in callback_failed handler: #{ex}"
80
- ex.backtrace.each { |b| warn b }
95
+ warn "From: #{ex.backtrace.first}"
81
96
  else
82
97
  trigger_callbacks :callback_failed, ex, named
83
98
  end
84
99
  end
85
100
 
101
+ def trigger_callbacks named, *args, &other
102
+ guard_callback named, other, *args
103
+ guard_callback named, @callbacks[named], *args
104
+ end
105
+
86
106
  def flush_and_close
87
107
  _write while connected? && @w_buff.buffered?
88
108
  io_close
@@ -109,25 +129,26 @@ private
109
129
  def _write
110
130
  written = 0
111
131
  while written < MAX_BYTES_PER_WRITE
112
- bytes, cb = @w_buff.shift
132
+ bytes, cb, cb_args = @w_buff.shift
113
133
  break unless bytes
114
134
  begin
115
135
  w = io_write bytes
116
136
  rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
117
137
  # writing will either block, or cannot otherwise be completed,
118
138
  # put data back and try again some other day
119
- @w_buff.unshift bytes, cb
139
+ @w_buff.unshift bytes, cb, cb_args
120
140
  break
121
141
  rescue Exception
122
142
  force_close $!
123
143
  break
124
144
  end
125
145
  written += w
146
+ trigger_callbacks :wrote, bytes, w
126
147
  if w < bytes.size
127
- @w_buff.unshift bytes[w..-1], cb
128
- trigger_callbacks :wrote, bytes, w
148
+ @w_buff.unshift bytes[w..-1], cb, cb_args
129
149
  else
130
- trigger_callbacks :wrote, bytes, w, &cb
150
+ # A separate callback invocation so we can pass custom args
151
+ guard_callback :wrote, cb, bytes, w, *cb_args
131
152
  end
132
153
  end
133
154
  end
@@ -0,0 +1,16 @@
1
+ module IoUnblock
2
+ class TcpSocket
3
+ extend Forwardable
4
+
5
+ def_delegators :@stream, :start, :stop, :callbacks, :write,
6
+ :running?, :connected?, :alive?, :select_delay, :select_delay=
7
+ attr_reader :host, :port
8
+
9
+ def initialize host, port, callbacks=nil
10
+ @host = host
11
+ @port = port
12
+ @socket = ::TCPSocket.new host, port
13
+ @stream = Stream.new @socket, callbacks
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  module IoUnblock
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -6,41 +6,41 @@ describe IoUnblock::Buffer do
6
6
  end
7
7
 
8
8
  it "should be empty when nothing is buffered" do
9
- @buffer.push :a, 1
9
+ @buffer.push :a, 1, 'hello'
10
10
  @buffer.pop
11
11
  @buffer.empty?.must_equal true
12
12
  @buffer.buffered?.must_equal false
13
13
  end
14
14
 
15
15
  it "should not be empty when stuff is buffered" do
16
- @buffer.push :a, 1
16
+ @buffer.push :a, 1, 'hello'
17
17
  @buffer.empty?.must_equal false
18
18
  @buffer.buffered?.must_equal true
19
19
  end
20
20
 
21
21
  it "should push to the end of the buffer" do
22
- @buffer.push :a, 1
23
- @buffer.push :b, 2
24
- @buffer.first.must_equal [:a, 1]
25
- @buffer.last.must_equal [:b, 2]
22
+ @buffer.push :a, 1, 'hello'
23
+ @buffer.push :b, 2, 'world'
24
+ @buffer.first.must_equal [:a, 1, 'hello']
25
+ @buffer.last.must_equal [:b, 2, 'world']
26
26
  end
27
27
 
28
28
  it "should unshift to the beginning of the buffer" do
29
- @buffer.unshift :a, 1
30
- @buffer.unshift :b, 2
31
- @buffer.last.must_equal [:a, 1]
32
- @buffer.first.must_equal [:b, 2]
29
+ @buffer.unshift :a, 1, 'hello'
30
+ @buffer.unshift :b, 2, 'world'
31
+ @buffer.first.must_equal [:b, 2, 'world']
32
+ @buffer.last.must_equal [:a, 1, 'hello']
33
33
  end
34
34
 
35
35
  it "should shift from the beginning" do
36
- @buffer.push :a, 1
37
- @buffer.push :b, 2
38
- @buffer.shift.must_equal [:a, 1]
36
+ @buffer.push :a, 1, 'hello'
37
+ @buffer.push :b, 2, 'world'
38
+ @buffer.shift.must_equal [:a, 1, 'hello']
39
39
  end
40
40
 
41
41
  it "should pop from the end" do
42
- @buffer.push :a, 1
43
- @buffer.push :b, 2
44
- @buffer.pop.must_equal [:b, 2]
42
+ @buffer.push :a, 1, 'hello'
43
+ @buffer.push :b, 2, 'world'
44
+ @buffer.pop.must_equal [:b, 2, 'world']
45
45
  end
46
46
  end
@@ -1,8 +1,19 @@
1
1
  require File.expand_path("../../spec_helper.rb", __FILE__)
2
2
 
3
3
  describe IoUnblock::Stream do
4
+ include Stubz
4
5
  def dummy_io; @dummy_io ||= DummyIO.new; end
5
6
  def stream; @stream ||= IoUnblock::Stream.new dummy_io; end
7
+
8
+ # Watch for Thread exceptions
9
+ before do
10
+ @th_abort = Thread.abort_on_exception
11
+ Thread.abort_on_exception = true
12
+ end
13
+
14
+ after do
15
+ Thread.abort_on_exception = @th_abort
16
+ end
6
17
 
7
18
  it "raises an exception if started twice" do
8
19
  stream.start
@@ -179,6 +190,14 @@ describe IoUnblock::Stream do
179
190
  called_with.must_equal [ ['lo', 2] ]
180
191
  end
181
192
 
193
+ it "triggers the given callback after writing the full string with params" do
194
+ dummy_io.max_write = 3
195
+ stream.start
196
+ stream.write('hello', 'turbo', 'is', 'me', &callback)
197
+ stream.stop
198
+ called_with.must_equal [ ['lo', 2, 'turbo', 'is', 'me'] ]
199
+ end
200
+
182
201
  it "is not connected when failed is triggered" do
183
202
  is_connected = true
184
203
  cb_stream = callback_stream(
@@ -202,13 +221,29 @@ describe IoUnblock::Stream do
202
221
  end
203
222
 
204
223
  it "warns when triggering callback_failed raises an exception" do
224
+ warn_lines = []
205
225
  cb_err = RuntimeError.new 'failback!'
206
226
  cb_stream = callback_stream(
207
227
  :callback_failed => lambda { |*_| raise cb_err }
208
228
  )
229
+ stub(cb_stream, :warn) { |w| warn_lines << w }
209
230
  cb_stream.start
210
231
  cb_stream.write('another test') { raise cb_err }
211
232
  cb_stream.stop
233
+ warn_lines.first.must_equal 'Exception raised in callback_failed handler: failback!'
234
+ /\AFrom: /.must_match warn_lines.last
235
+ end
236
+
237
+ it "stops properly when called within the processor thread" do
238
+ stopped_called = false
239
+ cb_stream = callback_stream(
240
+ :looped => lambda { |s| sleep 0.25; s.stop },
241
+ :stopped => lambda { |s| stopped_called = true }
242
+ )
243
+ cb_stream.start
244
+ Thread.pass until cb_stream.running?
245
+ Thread.pass while cb_stream.alive?
246
+ stopped_called.must_equal true
212
247
  end
213
248
  end
214
249
  end
@@ -0,0 +1,70 @@
1
+ require File.expand_path("../../spec_helper.rb", __FILE__)
2
+
3
+ describe IoUnblock::TcpSocket do
4
+ include Stubz
5
+
6
+ def tcp_socket
7
+ @tcp_socket ||= IoUnblock::TcpSocket.new('host.name', 33, {:call => :back})
8
+ end
9
+
10
+ before do
11
+ @socket = MiniTest::Mock.new
12
+ @stream = MiniTest::Mock.new
13
+ stub(::TCPSocket, :new, @socket)
14
+ stub(::IoUnblock::Stream, :new, @stream)
15
+ end
16
+
17
+ describe "attribs" do
18
+ it "has a host" do
19
+ tcp_socket.host.must_equal 'host.name'
20
+ end
21
+
22
+ it "has a port" do
23
+ tcp_socket.port.must_equal 33
24
+ end
25
+ end
26
+
27
+ describe "delegation to the stream" do
28
+ it "delegates start" do
29
+ @stream.expect(:start, nil, [:some, :args])
30
+ tcp_socket.start :some, :args
31
+ @stream.verify
32
+ end
33
+
34
+ it "delegates stop" do
35
+ @stream.expect(:stop, nil, ['moar cowbell'])
36
+ tcp_socket.stop 'moar cowbell'
37
+ @stream.verify
38
+ end
39
+
40
+ it "delegates callbacks" do
41
+ @stream.expect(:callbacks, {:a => :hash})
42
+ tcp_socket.callbacks.must_equal({:a => :hash})
43
+ @stream.verify
44
+ end
45
+
46
+ it "delegates running?" do
47
+ @stream.expect(:running?, :generally_a_bool)
48
+ tcp_socket.running?.must_equal :generally_a_bool
49
+ @stream.verify
50
+ end
51
+
52
+ it "delegates connected?" do
53
+ @stream.expect(:connected?, :again_a_bool)
54
+ tcp_socket.connected?.must_equal :again_a_bool
55
+ @stream.verify
56
+ end
57
+
58
+ it "delegates select_delay" do
59
+ @stream.expect(:select_delay, 48)
60
+ tcp_socket.select_delay.must_equal 48
61
+ @stream.verify
62
+ end
63
+
64
+ it "delegates select_delay=" do
65
+ @stream.expect(:select_delay=, 19, [19])
66
+ tcp_socket.select_delay = 19
67
+ @stream.verify
68
+ end
69
+ end
70
+ end
@@ -11,74 +11,10 @@ begin
11
11
  require 'minitest/emoji'
12
12
  rescue LoadError
13
13
  end
14
- require 'io_unblock'
15
- require 'stringio'
16
14
 
17
- # Used to test IO stuff, using stringio objects for the write and
18
- # read stream.
19
- class DummyIO
20
- attr_reader :closed, :w_stream, :r_stream, :raised_write, :raised_read
21
- alias :closed? :closed
22
- alias :raised_write? :raised_write
23
- alias :raised_read? :raised_read
24
- attr_accessor :readable, :writeable
25
- attr_accessor :write_delay, :read_delay
26
- attr_accessor :max_write, :max_read
27
- attr_accessor :raise_read, :raise_write
28
- alias :readable? :readable
29
- alias :writeable? :writeable
30
-
31
- def initialize *args, &block
32
- @r_stream = StringIO.new
33
- @w_stream = StringIO.new
34
- @readable = @writeable = true
35
- @read_delay = @write_delay = 0
36
- @max_write = 0
37
- @max_read = 0
38
- @closed = false
39
- @raise_read = nil
40
- @raise_write = nil
41
- @raised_write = false
42
- @raised_read = false
43
- end
44
-
45
- def close
46
- @closed = true
47
- @w_stream.close
48
- @r_stream.close
49
- end
50
-
51
- def write_nonblock bytes
52
- sleep(@write_delay) if @write_delay > 0
53
- do_raise_write
54
- if @max_write > 0 && bytes.size > @max_write
55
- @w_stream.write bytes[0...@max_write]
56
- else
57
- @w_stream.write bytes
58
- end
59
- end
60
-
61
- def read_nonblock len
62
- sleep(@read_delay) if @read_delay > 0
63
- do_raise_read
64
- if @max_read > 0 && len > @max_read
65
- @r_stream.read @max_read
66
- else
67
- @r_stream.read len
68
- end
69
- end
15
+ Dir[File.expand_path('../support/*.rb', __FILE__)].each do |r|
16
+ require r
17
+ end
70
18
 
71
- def do_raise_write
72
- if @raise_write
73
- @raised_write = true
74
- raise @raise_write
75
- end
76
- end
19
+ require 'io_unblock'
77
20
 
78
- def do_raise_read
79
- if @raise_read
80
- @raised_read = true
81
- raise @raise_read
82
- end
83
- end
84
- end
@@ -0,0 +1,70 @@
1
+ require 'stringio'
2
+
3
+ # Used to test IO stuff, using stringio objects for the write and
4
+ # read stream.
5
+ class DummyIO
6
+ attr_reader :closed, :w_stream, :r_stream, :raised_write, :raised_read
7
+ alias :closed? :closed
8
+ alias :raised_write? :raised_write
9
+ alias :raised_read? :raised_read
10
+ attr_accessor :readable, :writeable
11
+ attr_accessor :write_delay, :read_delay
12
+ attr_accessor :max_write, :max_read
13
+ attr_accessor :raise_read, :raise_write
14
+ alias :readable? :readable
15
+ alias :writeable? :writeable
16
+
17
+ def initialize *args, &block
18
+ @r_stream = StringIO.new
19
+ @w_stream = StringIO.new
20
+ @readable = @writeable = true
21
+ @read_delay = @write_delay = 0
22
+ @max_write = 0
23
+ @max_read = 0
24
+ @closed = false
25
+ @raise_read = nil
26
+ @raise_write = nil
27
+ @raised_write = false
28
+ @raised_read = false
29
+ end
30
+
31
+ def close
32
+ @closed = true
33
+ @w_stream.close
34
+ @r_stream.close
35
+ end
36
+
37
+ def write_nonblock bytes
38
+ sleep(@write_delay) if @write_delay > 0
39
+ do_raise_write
40
+ if @max_write > 0 && bytes.size > @max_write
41
+ @w_stream.write bytes[0...@max_write]
42
+ else
43
+ @w_stream.write bytes
44
+ end
45
+ end
46
+
47
+ def read_nonblock len
48
+ sleep(@read_delay) if @read_delay > 0
49
+ do_raise_read
50
+ if @max_read > 0 && len > @max_read
51
+ @r_stream.read @max_read
52
+ else
53
+ @r_stream.read len
54
+ end
55
+ end
56
+
57
+ def do_raise_write
58
+ if @raise_write
59
+ @raised_write = true
60
+ raise @raise_write
61
+ end
62
+ end
63
+
64
+ def do_raise_read
65
+ if @raise_read
66
+ @raised_read = true
67
+ raise @raise_read
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,49 @@
1
+ module Stubz
2
+ def self.included base
3
+ base.__send__(:before) { _ensure_stubs_ }
4
+ base.__send__(:after) { _reset_stubs_ }
5
+ end
6
+
7
+ def stub_instance(klass, meth, *val, &block)
8
+ _do_stub_ klass, meth, *val, &block
9
+ end
10
+
11
+ def stub(obj, meth, *val, &block)
12
+ _do_stub_ get_meta_class(obj), meth, *val, &block
13
+ end
14
+
15
+ def get_meta_class klass
16
+ class << klass; self; end
17
+ end
18
+
19
+ def _do_stub_ klass, meth, *val, &block
20
+ if klass.method_defined? meth
21
+ aliased = "_stub_#{meth}_#{Time.now.to_i}_"
22
+ klass.send(:alias_method, aliased, meth)
23
+ else
24
+ aliased = nil
25
+ end
26
+ if block
27
+ klass.send(:define_method, meth, &block)
28
+ else
29
+ klass.send(:define_method, meth) do |*_|
30
+ val.first
31
+ end
32
+ end
33
+ @stubbies << [klass, meth, aliased]
34
+ end
35
+
36
+ def _ensure_stubs_
37
+ @stubbies = []
38
+ end
39
+
40
+ def _reset_stubs_
41
+ @stubbies.each do |(klass, meth, aliased)|
42
+ klass.send(:remove_method, meth)
43
+ if aliased
44
+ klass.send(:alias_method, meth, aliased)
45
+ klass.send(:remove_method, aliased)
46
+ end
47
+ end
48
+ end
49
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io_unblock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-13 00:00:00.000000000Z
12
+ date: 2012-03-15 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
16
- requirement: &2161186640 !ruby/object:Gem::Requirement
16
+ requirement: &2160167540 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2161186640
24
+ version_requirements: *2160167540
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &2161185800 !ruby/object:Gem::Requirement
27
+ requirement: &2160165920 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *2161185800
35
+ version_requirements: *2160165920
36
36
  description: Non-blocking IO reads/writes wrapped in a thread
37
37
  email:
38
38
  - ian.eccles@gmail.com
@@ -50,12 +50,17 @@ files:
50
50
  - lib/io_unblock.rb
51
51
  - lib/io_unblock/buffer.rb
52
52
  - lib/io_unblock/delegation.rb
53
+ - lib/io_unblock/errors.rb
53
54
  - lib/io_unblock/stream.rb
55
+ - lib/io_unblock/tcp_socket.rb
54
56
  - lib/io_unblock/version.rb
55
57
  - spec/io_unblock/buffer_spec.rb
56
58
  - spec/io_unblock/delegation_spec.rb
57
59
  - spec/io_unblock/stream_spec.rb
60
+ - spec/io_unblock/tcp_socket_spec.rb
58
61
  - spec/spec_helper.rb
62
+ - spec/support/dummy_io.rb
63
+ - spec/support/stubz.rb
59
64
  homepage: https://github.com/iande/io_unblock
60
65
  licenses: []
61
66
  post_install_message:
@@ -84,5 +89,8 @@ test_files:
84
89
  - spec/io_unblock/buffer_spec.rb
85
90
  - spec/io_unblock/delegation_spec.rb
86
91
  - spec/io_unblock/stream_spec.rb
92
+ - spec/io_unblock/tcp_socket_spec.rb
87
93
  - spec/spec_helper.rb
94
+ - spec/support/dummy_io.rb
95
+ - spec/support/stubz.rb
88
96
  has_rdoc: