io_unblock 0.1.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.
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: