io_unblock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,18 @@
1
+ # vim: set filetype=ruby :
2
+
3
+ require 'autotest/restart'
4
+ require 'autotest/growl'
5
+
6
+ Autotest.add_hook :initialize do |at|
7
+ at.testlib = 'minitest/spec'
8
+
9
+ at.clear_mappings
10
+ at.add_mapping(/^lib\/(.*)\.rb$/) { |f, m| "spec/#{m[1]}_spec.rb" }
11
+ at.add_mapping(/^spec\/.*_spec\.rb$/) { |f,_| f }
12
+ at.add_mapping(/^spec\/spec_helper\.rb$/) { |f,_|
13
+ at.files_matching(/^spec\/.*_spec\.rb$/)
14
+ }
15
+
16
+ at.add_exception '.git'
17
+ at.add_exception 'coverage'
18
+ end
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ coverage/*
6
+ *~
7
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in io_unblock.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'autotest'
8
+ gem 'minitest'
9
+ gem 'minitest-emoji'
10
+ end
11
+
12
+ group :human_testing do
13
+ gem 'autotest-growl'
14
+ gem 'simplecov'
15
+ end
data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # IoUnblock
2
+
3
+ An IO stream, a thread, and a handful of callbacks.
4
+
5
+ ## Purpose
6
+
7
+ This gem's primary purpose is to handle threaded non-blocking IO so
8
+ that the OnStomp gem is less distracted from its primary duty of
9
+ handling the STOMP protocol.
10
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "lib"
6
+ t.test_files = FileList['spec/**/*_spec.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ desc "Generate a coverage report from tests with simplecov"
11
+ task :coverage do
12
+ ENV['SCOV'] = '1'
13
+ Rake::Task['test'].invoke
14
+ end
15
+
16
+ task :default => [:test]
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "io_unblock/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "io_unblock"
7
+ s.version = IoUnblock::VERSION
8
+ s.authors = ["Ian D. Eccles"]
9
+ s.email = ["ian.eccles@gmail.com"]
10
+ s.homepage = "https://github.com/iande/io_unblock"
11
+ s.summary = %q{Non-blocking IO reads/writes wrapped in a thread}
12
+ s.description = %q{Non-blocking IO reads/writes wrapped in a thread}
13
+
14
+ s.rubyforge_project = "io_unblock"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency 'minitest'
22
+ s.add_development_dependency 'rake'
23
+ end
data/lib/io_unblock.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'thread'
2
+ require "io_unblock/version"
3
+
4
+ module IoUnblock
5
+ class IoUnblockError < StandardError; end
6
+ end
7
+
8
+ require 'io_unblock/delegation'
9
+ require 'io_unblock/buffer'
10
+ require 'io_unblock/stream'
@@ -0,0 +1,50 @@
1
+ module IoUnblock
2
+ # A very simple synchronized buffer.
3
+ #
4
+ # @api private
5
+ class Buffer
6
+
7
+ def initialize
8
+ @buffer = []
9
+ @mutex = Mutex.new
10
+ end
11
+
12
+ def push bytes, cb
13
+ synched { @buffer.push [bytes, cb] }
14
+ end
15
+
16
+ def pop
17
+ synched { @buffer.pop }
18
+ end
19
+
20
+ def unshift bytes, cb
21
+ synched { @buffer.unshift [bytes, cb] }
22
+ end
23
+
24
+ def shift
25
+ synched { @buffer.shift }
26
+ end
27
+
28
+ def first
29
+ synched { @buffer.first }
30
+ end
31
+
32
+ def last
33
+ synched { @buffer.last }
34
+ end
35
+
36
+ def empty?
37
+ # Should we lock?
38
+ @buffer.empty?
39
+ end
40
+
41
+ def buffered?
42
+ !empty?
43
+ end
44
+
45
+ private
46
+ def synched &block
47
+ @mutex.synchronize(&block)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,91 @@
1
+ module IoUnblock
2
+ # Handles delegating read and write methods to their non-blocking
3
+ # counterparts on the IO object. If the IO object does not support
4
+ # non-blocking methods, falls back to blocking ones.
5
+ #
6
+ # @api private
7
+ module Delegation
8
+ module NonBlockingWrites
9
+ def io_write bytes; io.write_nonblock bytes; end
10
+ private :io_write
11
+ end
12
+
13
+ module BlockingWrites
14
+ def io_write bytes; io.write bytes; end
15
+ private :io_write
16
+ end
17
+
18
+ module ForwardWriteable
19
+ def writeable?; io.writeable?; end
20
+ private :writeable?
21
+ end
22
+
23
+ module SelectWriteable
24
+ def writeable?; !!IO.select(nil, io_selector, nil, select_delay); end
25
+ private :writeable?
26
+ end
27
+
28
+ module NonBlockingReads
29
+ def io_read len; io.read_nonblock len; end
30
+ private :io_read
31
+ end
32
+
33
+ module PartialReads
34
+ def io_read len; io.readpartial len; end
35
+ private :io_read
36
+ end
37
+
38
+ module BlockingReads
39
+ def io_read len; io.read len; end
40
+ private :io_read
41
+ end
42
+
43
+ module ForwardReadable
44
+ def readable?; io.readable?; end
45
+ private :readable?
46
+ end
47
+
48
+ module SelectReadable
49
+ def readable?; !!IO.select(io_selector, nil, nil, select_delay); end
50
+ private :readable?
51
+ end
52
+
53
+ class << self
54
+ def define_io_methods stream
55
+ define_io_write stream
56
+ define_io_read stream
57
+ end
58
+
59
+ private
60
+ def define_io_write stream
61
+ if stream.io.respond_to? :write_nonblock
62
+ stream.extend NonBlockingWrites
63
+ else
64
+ stream.extend BlockingWrites
65
+ end
66
+
67
+ if stream.io.respond_to? :writeable?
68
+ stream.extend ForwardWriteable
69
+ else
70
+ stream.extend SelectWriteable
71
+ end
72
+ end
73
+
74
+ def define_io_read stream
75
+ if stream.io.respond_to? :read_nonblock
76
+ stream.extend NonBlockingReads
77
+ elsif stream.io.respond_to? :readpartial
78
+ stream.extend PartialReads
79
+ else
80
+ stream.extend BlockingReads
81
+ end
82
+
83
+ if stream.io.respond_to? :readable?
84
+ stream.extend ForwardReadable
85
+ else
86
+ stream.extend SelectReadable
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,158 @@
1
+ module IoUnblock
2
+ class StreamError < IoUnblockError; end
3
+
4
+ class Stream
5
+ MAX_BYTES_PER_WRITE = 1024 * 8
6
+ MAX_BYTES_PER_READ = 1024 * 4
7
+
8
+ attr_reader :running, :connected, :io, :io_selector, :callbacks
9
+ attr_accessor :select_delay
10
+ alias :running? :running
11
+ alias :connected? :connected
12
+
13
+ # The given IO object, `io`, is assumed to be opened/connected.
14
+ # Global callbacks:
15
+ # - failed: called when IO access throws an exception that cannot
16
+ # be recovered from (opening the IO fails,
17
+ # TCP connection reset, unexpected EOF, etc.)
18
+ # - read: called when any data is read from the underlying IO
19
+ # object
20
+ # - wrote: called when any data is written to the underlying IO
21
+ # object
22
+ # - closed: called when the underlying IO object is closed (even
23
+ # if it is closed as a result of a failure)
24
+ # - started: called when the IO processing has started
25
+ # - stopped: called when the IO processing has stopped
26
+ # - looped: called when each time the IO processing loops
27
+ def initialize io, callbacks=nil
28
+ @io = io
29
+ @io_selector = [@io]
30
+ @processor = nil
31
+ @s_mutex = Mutex.new
32
+ @w_buff = IoUnblock::Buffer.new
33
+ @running = false
34
+ @connected = true
35
+ @callbacks = callbacks || {}
36
+ @select_delay = 0.1
37
+ Delegation.define_io_methods self
38
+ yield self if block_given?
39
+ end
40
+
41
+ def start &cb
42
+ @s_mutex.synchronize do
43
+ raise StreamError, "already started" if @running
44
+ @running = true
45
+ @processor = Thread.new do
46
+ trigger_callbacks :started, :start, &cb
47
+ read_and_write while running? && connected?
48
+ flush_and_close
49
+ trigger_callbacks :stopped, :stop, &cb
50
+ end
51
+ end
52
+ self
53
+ end
54
+
55
+ def stop
56
+ @s_mutex.synchronize do
57
+ if @running
58
+ @running = false
59
+ @processor.join
60
+ end
61
+ end
62
+ self
63
+ end
64
+
65
+ # The callback triggered here will be invoked only when all bytes
66
+ # have been written.
67
+ def write bytes, &cb
68
+ @w_buff.push bytes, cb
69
+ self
70
+ end
71
+
72
+ private
73
+ def trigger_callbacks named, *args, &other
74
+ other && other.call(*args)
75
+ @callbacks.key?(named) && @callbacks[named].call(*args)
76
+ end
77
+
78
+ def flush_and_close
79
+ _write while connected? && @w_buff.buffered?
80
+ io_close
81
+ self
82
+ end
83
+
84
+ def read_and_write
85
+ _read if read?
86
+ _write if write?
87
+ trigger_callbacks :looped, self
88
+ self
89
+ end
90
+
91
+ def _read
92
+ begin
93
+ bytes = io_read MAX_BYTES_PER_READ
94
+ trigger_callbacks(:read, bytes) if bytes
95
+ rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
96
+ rescue Exception
97
+ force_close $!
98
+ end
99
+ end
100
+
101
+ def _write
102
+ written = 0
103
+ while written < MAX_BYTES_PER_WRITE
104
+ bytes, cb = @w_buff.shift
105
+ break unless bytes
106
+ begin
107
+ w = io_write bytes
108
+ rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
109
+ # writing will either block, or cannot otherwise be completed,
110
+ # put data back and try again some other day
111
+ @w_buff.unshift bytes, cb
112
+ break
113
+ rescue Exception
114
+ force_close $!
115
+ break
116
+ end
117
+ written += w
118
+ if w < bytes.size
119
+ @w_buff.unshift bytes[w..-1], cb
120
+ trigger_callbacks :wrote, bytes, w
121
+ else
122
+ trigger_callbacks :wrote, bytes, w, &cb
123
+ end
124
+ end
125
+ end
126
+
127
+ def io_close
128
+ if connected?
129
+ @io.close rescue nil
130
+ @connected = false
131
+ trigger_callbacks :closed
132
+ end
133
+ end
134
+
135
+ def force_close ex
136
+ io_close
137
+ trigger_callbacks :failed, ex
138
+ end
139
+
140
+ def write?
141
+ begin
142
+ connected? && @w_buff.buffered? && writeable?
143
+ rescue Exception
144
+ force_close $!
145
+ false
146
+ end
147
+ end
148
+
149
+ def read?
150
+ begin
151
+ connected? && readable?
152
+ rescue Exception
153
+ force_close $!
154
+ false
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,3 @@
1
+ module IoUnblock
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,46 @@
1
+ require File.expand_path("../../spec_helper.rb", __FILE__)
2
+
3
+ describe IoUnblock::Buffer do
4
+ before do
5
+ @buffer = IoUnblock::Buffer.new
6
+ end
7
+
8
+ it "should be empty when nothing is buffered" do
9
+ @buffer.push :a, 1
10
+ @buffer.pop
11
+ @buffer.empty?.must_equal true
12
+ @buffer.buffered?.must_equal false
13
+ end
14
+
15
+ it "should not be empty when stuff is buffered" do
16
+ @buffer.push :a, 1
17
+ @buffer.empty?.must_equal false
18
+ @buffer.buffered?.must_equal true
19
+ end
20
+
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]
26
+ end
27
+
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]
33
+ end
34
+
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]
39
+ end
40
+
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]
45
+ end
46
+ end
@@ -0,0 +1,88 @@
1
+ require File.expand_path("../../spec_helper.rb", __FILE__)
2
+
3
+ describe IoUnblock::Delegation do
4
+ class FakeStream
5
+ attr_reader :io
6
+ def initialize io
7
+ @io = io
8
+ end
9
+ def io_selector; [@io]; end
10
+ def select_delay; 18; end
11
+ end
12
+
13
+ before do
14
+ @mock_io = MiniTest::Mock.new
15
+ @stream = FakeStream.new @mock_io
16
+ end
17
+
18
+ it "delegates to read_nonblock when available" do
19
+ @mock_io.expect(:read_nonblock, :reading, [42])
20
+ IoUnblock::Delegation.define_io_methods @stream
21
+ @stream.__send__ :io_read, 42
22
+ @mock_io.verify
23
+ end
24
+
25
+ it "delegates to readpartial when available" do
26
+ @mock_io.expect(:readpartial, :reading, [42])
27
+ IoUnblock::Delegation.define_io_methods @stream
28
+ @stream.__send__ :io_read, 42
29
+ @mock_io.verify
30
+ end
31
+
32
+ it "delegates to read if all else fails" do
33
+ @mock_io.expect(:read, :reading, [42])
34
+ IoUnblock::Delegation.define_io_methods @stream
35
+ @stream.__send__ :io_read, 42
36
+ @mock_io.verify
37
+ end
38
+
39
+ it "delegates to write_nonblock when available" do
40
+ @mock_io.expect(:write_nonblock, :writing, ['hello'])
41
+ IoUnblock::Delegation.define_io_methods @stream
42
+ @stream.__send__ :io_write, 'hello'
43
+ @mock_io.verify
44
+ end
45
+
46
+ it "delegates to write if all else fails" do
47
+ @mock_io.expect(:write, :writing, ['hello'])
48
+ IoUnblock::Delegation.define_io_methods @stream
49
+ @stream.__send__ :io_write, 'hello'
50
+ @mock_io.verify
51
+ end
52
+
53
+ it "delegates to IO.select when there is no readable? method" do
54
+ mocked = MiniTest::Mock.new
55
+ mocked.expect(:select, nil, [[@mock_io], nil, nil, 18])
56
+ real_io = IO
57
+ ::IO = mocked
58
+ IoUnblock::Delegation.define_io_methods @stream
59
+ @stream.__send__ :readable?
60
+ ::IO = real_io
61
+ mocked.verify
62
+ end
63
+
64
+ it "delegates to IO.select when there is no writeable? method" do
65
+ mocked = MiniTest::Mock.new
66
+ mocked.expect(:select, nil, [nil, [@mock_io], nil, 18])
67
+ real_io = IO
68
+ ::IO = mocked
69
+ IoUnblock::Delegation.define_io_methods @stream
70
+ @stream.__send__ :writeable?
71
+ ::IO = real_io
72
+ mocked.verify
73
+ end
74
+
75
+ it "delegates to readable? when the method is available" do
76
+ @mock_io.expect(:readable?, false, [])
77
+ IoUnblock::Delegation.define_io_methods @stream
78
+ @stream.__send__ :readable?
79
+ @mock_io.verify
80
+ end
81
+
82
+ it "delegates to writeable? when the method is available" do
83
+ @mock_io.expect(:writeable?, false, [])
84
+ IoUnblock::Delegation.define_io_methods @stream
85
+ @stream.__send__ :writeable?
86
+ @mock_io.verify
87
+ end
88
+ end
@@ -0,0 +1,199 @@
1
+ require File.expand_path("../../spec_helper.rb", __FILE__)
2
+ require 'stringio'
3
+ require 'logger'
4
+
5
+ $log = Logger.new $stdout
6
+
7
+ describe IoUnblock::Stream do
8
+ def dummy_io; @dummy_io ||= DummyIO.new; end
9
+ def stream; @stream ||= IoUnblock::Stream.new dummy_io; end
10
+
11
+ it "raises an exception if started twice" do
12
+ stream.start
13
+ lambda {
14
+ stream.start
15
+ }.must_raise IoUnblock::StreamError
16
+ stream.stop
17
+ end
18
+
19
+ it "reads only if the IO is ready for it" do
20
+ dummy_io.r_stream.string = 'test string'
21
+ dummy_io.readable = false
22
+ stream.start
23
+ Thread.pass until stream.running?
24
+ dummy_io.r_stream.pos.must_equal 0
25
+ dummy_io.readable = true
26
+ Thread.pass until dummy_io.r_stream.eof?
27
+ stream.stop
28
+ dummy_io.r_stream.pos.must_equal 11
29
+ end
30
+
31
+ it "closes the io when stopping" do
32
+ dummy_io.closed?.must_equal false
33
+ stream.start
34
+ Thread.pass until stream.running?
35
+ stream.stop
36
+ dummy_io.closed?.must_equal true
37
+ end
38
+
39
+ it "flushes all writes before stopping (even if io claims to be unwriteable)" do
40
+ dummy_io.writeable = false
41
+ stream.start
42
+ stream.write 'hello '
43
+ stream.write 'world.'
44
+ Thread.pass until stream.running?
45
+ dummy_io.w_stream.string.must_equal ''
46
+ stream.stop
47
+ dummy_io.w_stream.string.must_equal 'hello world.'
48
+ end
49
+
50
+ it "does not die on EINTER" do
51
+ dummy_io.raise_write = Errno::EINTR.new
52
+ dummy_io.raise_read = Errno::EINTR.new
53
+ stream.start
54
+ stream.write 'hello'
55
+ Thread.pass until dummy_io.raised_read?
56
+ stream.connected?.must_equal true
57
+ dummy_io.raise_read = nil
58
+ Thread.pass until dummy_io.raised_write?
59
+ stream.connected?.must_equal true
60
+ dummy_io.raise_write = nil
61
+ stream.stop
62
+ dummy_io.w_stream.string.must_equal 'hello'
63
+ end
64
+
65
+ it "does not die on EAGAIN" do
66
+ dummy_io.raise_write = Errno::EAGAIN.new
67
+ dummy_io.raise_read = Errno::EINTR.new
68
+ stream.start
69
+ stream.write 'hello'
70
+ Thread.pass until dummy_io.raised_read?
71
+ stream.connected?.must_equal true
72
+ dummy_io.raise_read = nil
73
+ Thread.pass until dummy_io.raised_write?
74
+ stream.connected?.must_equal true
75
+ dummy_io.raise_write = nil
76
+ stream.stop
77
+ dummy_io.w_stream.string.must_equal 'hello'
78
+ end
79
+
80
+ it "does not die on EWOULDBLOCK" do
81
+ dummy_io.raise_write = Errno::EWOULDBLOCK.new
82
+ dummy_io.raise_read = Errno::EINTR.new
83
+ stream.start
84
+ stream.write 'hello'
85
+ Thread.pass until dummy_io.raised_read?
86
+ stream.connected?.must_equal true
87
+ dummy_io.raise_read = nil
88
+ Thread.pass until dummy_io.raised_write?
89
+ stream.connected?.must_equal true
90
+ dummy_io.raise_write = nil
91
+ stream.stop
92
+ dummy_io.w_stream.string.must_equal 'hello'
93
+ end
94
+
95
+ describe "callbacks" do
96
+ def called_with; @calls_received ||= []; end
97
+ def callback; @callback ||= lambda { |*a| called_with << a }; end
98
+
99
+ def callback_stream cbs=nil
100
+ IoUnblock::Stream.new(dummy_io, cbs)
101
+ end
102
+
103
+ it "triggers started when starting" do
104
+ cb_stream = callback_stream(started: callback)
105
+ cb_stream.start
106
+ cb_stream.stop
107
+ called_with.must_equal [ [:start] ]
108
+ end
109
+
110
+ it "triggers stopped when stopping" do
111
+ cb_stream = callback_stream(stopped: callback)
112
+ cb_stream.start
113
+ cb_stream.stop
114
+ called_with.must_equal [ [:stop] ]
115
+ end
116
+
117
+ it "triggers looped after each read/write cycle" do
118
+ cb_stream = callback_stream(looped: callback)
119
+ cb_stream.start
120
+ Thread.pass while called_with.empty?
121
+ cb_stream.stop
122
+ called_with.first.must_equal [cb_stream]
123
+ end
124
+
125
+ it "triggers wrote when writing" do
126
+ dummy_io.max_write = 3
127
+ cb_stream = callback_stream(wrote: callback)
128
+ cb_stream.start
129
+ cb_stream.write "hello"
130
+ cb_stream.stop
131
+ called_with.must_equal [ ['hello', 3], ['lo', 2] ]
132
+ end
133
+
134
+ it "triggers read when reading" do
135
+ dummy_io.max_read = 3
136
+ dummy_io.r_stream.string = 'hello'
137
+ cb_stream = callback_stream(read: callback)
138
+ cb_stream.start
139
+ Thread.pass until dummy_io.r_stream.eof?
140
+ cb_stream.stop
141
+ called_with.must_equal [ ['hel'], ['lo'] ]
142
+ end
143
+
144
+ it "triggers closed when closing" do
145
+ cb_stream = callback_stream(closed: callback)
146
+ cb_stream.start
147
+ cb_stream.stop
148
+ called_with.must_equal [ [] ]
149
+ end
150
+
151
+ it "triggers failed when reading raises an error" do
152
+ err = RuntimeError.new "fail"
153
+ cb_stream = callback_stream(failed: callback)
154
+ dummy_io.raise_read = err
155
+ cb_stream.start
156
+ Thread.pass until cb_stream.running?
157
+ Thread.pass while cb_stream.connected?
158
+ cb_stream.stop
159
+ called_with.must_equal [ [err] ]
160
+ end
161
+
162
+ it "triggers failed when writing raises an error" do
163
+ err = RuntimeError.new "fail"
164
+ cb_stream = callback_stream(failed: callback)
165
+ dummy_io.raise_write = err
166
+ cb_stream.start
167
+ cb_stream.write "hello"
168
+ cb_stream.stop
169
+ called_with.must_equal [ [err] ]
170
+ end
171
+
172
+ it "triggers the given callback when starting and stopping" do
173
+ stream.start(&callback)
174
+ stream.stop
175
+ called_with.must_equal [ [:start], [:stop]]
176
+ end
177
+
178
+ it "triggers the given callback after writing the full string" do
179
+ dummy_io.max_write = 3
180
+ stream.start
181
+ stream.write('hello', &callback)
182
+ stream.stop
183
+ called_with.must_equal [ ['lo', 2] ]
184
+ end
185
+
186
+ it "is not connected when failed is triggered" do
187
+ is_connected = true
188
+ cb_stream = callback_stream(
189
+ failed: lambda { |ex| is_connected = cb_stream.connected? }
190
+ )
191
+ err = RuntimeError.new "fail"
192
+ dummy_io.raise_write = err
193
+ cb_stream.start
194
+ cb_stream.write "hello"
195
+ cb_stream.stop
196
+ is_connected.must_equal false
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,81 @@
1
+ if ENV['SCOV']
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ puts "I am started?! #{Process.pid}"
5
+ add_filter "/spec/"
6
+ end
7
+ end
8
+
9
+ require 'minitest/autorun'
10
+ require 'minitest/emoji'
11
+ require 'io_unblock'
12
+ require 'stringio'
13
+
14
+ # Used to test IO stuff, using stringio objects for the write and
15
+ # read stream.
16
+ class DummyIO
17
+ attr_reader :closed, :w_stream, :r_stream, :raised_write, :raised_read
18
+ alias :closed? :closed
19
+ alias :raised_write? :raised_write
20
+ alias :raised_read? :raised_read
21
+ attr_accessor :readable, :writeable
22
+ attr_accessor :write_delay, :read_delay
23
+ attr_accessor :max_write, :max_read
24
+ attr_accessor :raise_read, :raise_write
25
+ alias :readable? :readable
26
+ alias :writeable? :writeable
27
+
28
+ def initialize *args, &block
29
+ @r_stream = StringIO.new
30
+ @w_stream = StringIO.new
31
+ @readable = @writeable = true
32
+ @read_delay = @write_delay = 0
33
+ @max_write = 0
34
+ @max_read = 0
35
+ @closed = false
36
+ @raise_read = nil
37
+ @raise_write = nil
38
+ @raised_write = false
39
+ @raised_read = false
40
+ end
41
+
42
+ def close
43
+ @closed = true
44
+ @w_stream.close
45
+ @r_stream.close
46
+ end
47
+
48
+ def write_nonblock bytes
49
+ sleep(@write_delay) if @write_delay > 0
50
+ do_raise_write
51
+ if @max_write > 0 && bytes.size > @max_write
52
+ @w_stream.write bytes[0...@max_write]
53
+ else
54
+ @w_stream.write bytes
55
+ end
56
+ end
57
+
58
+ def read_nonblock len
59
+ sleep(@read_delay) if @read_delay > 0
60
+ do_raise_read
61
+ if @max_read > 0 && len > @max_read
62
+ @r_stream.read @max_read
63
+ else
64
+ @r_stream.read len
65
+ end
66
+ end
67
+
68
+ def do_raise_write
69
+ if @raise_write
70
+ @raised_write = true
71
+ raise @raise_write
72
+ end
73
+ end
74
+
75
+ def do_raise_read
76
+ if @raise_read
77
+ @raised_read = true
78
+ raise @raise_read
79
+ end
80
+ end
81
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: io_unblock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ian D. Eccles
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-12 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: &2161332520 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2161332520
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &2161331860 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2161331860
36
+ description: Non-blocking IO reads/writes wrapped in a thread
37
+ email:
38
+ - ian.eccles@gmail.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - .autotest
44
+ - .gitignore
45
+ - Gemfile
46
+ - README.md
47
+ - Rakefile
48
+ - io_unblock.gemspec
49
+ - lib/io_unblock.rb
50
+ - lib/io_unblock/buffer.rb
51
+ - lib/io_unblock/delegation.rb
52
+ - lib/io_unblock/stream.rb
53
+ - lib/io_unblock/version.rb
54
+ - spec/io_unblock/buffer_spec.rb
55
+ - spec/io_unblock/delegation_spec.rb
56
+ - spec/io_unblock/stream_spec.rb
57
+ - spec/spec_helper.rb
58
+ homepage: https://github.com/iande/io_unblock
59
+ licenses: []
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project: io_unblock
78
+ rubygems_version: 1.8.12
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Non-blocking IO reads/writes wrapped in a thread
82
+ test_files:
83
+ - spec/io_unblock/buffer_spec.rb
84
+ - spec/io_unblock/delegation_spec.rb
85
+ - spec/io_unblock/stream_spec.rb
86
+ - spec/spec_helper.rb
87
+ has_rdoc: